@openclaw/feishu 2026.2.24 → 2026.3.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/index.ts +2 -0
- package/package.json +2 -1
- package/skills/feishu-doc/SKILL.md +109 -3
- package/src/accounts.test.ts +90 -0
- package/src/accounts.ts +11 -2
- package/src/async.ts +62 -0
- package/src/bitable.ts +189 -215
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +55 -0
- package/src/bot.test.ts +863 -9
- package/src/bot.ts +414 -200
- package/src/card-action.ts +79 -0
- package/src/channel.ts +6 -0
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +107 -0
- package/src/client.ts +13 -0
- package/src/config-schema.test.ts +82 -1
- package/src/config-schema.ts +54 -3
- package/src/doc-schema.ts +141 -0
- package/src/docx-batch-insert.ts +190 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +76 -0
- package/src/docx.test.ts +470 -0
- package/src/docx.ts +996 -72
- package/src/drive.ts +38 -33
- package/src/media.test.ts +123 -6
- package/src/media.ts +31 -10
- package/src/monitor.account.ts +286 -0
- package/src/monitor.reaction.test.ts +235 -0
- package/src/monitor.startup.test.ts +187 -0
- package/src/monitor.startup.ts +51 -0
- package/src/monitor.state.ts +76 -0
- package/src/monitor.transport.ts +163 -0
- package/src/monitor.ts +44 -346
- package/src/monitor.webhook-security.test.ts +27 -1
- package/src/outbound.test.ts +181 -0
- package/src/outbound.ts +94 -7
- package/src/perm.ts +37 -30
- package/src/policy.test.ts +56 -1
- package/src/policy.ts +5 -1
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +253 -0
- package/src/probe.ts +99 -7
- package/src/reply-dispatcher.test.ts +259 -0
- package/src/reply-dispatcher.ts +139 -45
- package/src/send.reply-fallback.test.ts +105 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +143 -18
- package/src/streaming-card.ts +131 -43
- package/src/targets.test.ts +26 -1
- package/src/targets.ts +11 -6
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +140 -10
- package/src/wiki.ts +55 -50
package/src/reply-dispatcher.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "openclaw/plugin-sdk";
|
|
9
9
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
10
10
|
import { createFeishuClient } from "./client.js";
|
|
11
|
+
import { sendMediaFeishu } from "./media.js";
|
|
11
12
|
import type { MentionTarget } from "./mention.js";
|
|
12
13
|
import { buildMentionedCardContent } from "./mention.js";
|
|
13
14
|
import { getFeishuRuntime } from "./runtime.js";
|
|
@@ -21,35 +22,85 @@ function shouldUseCard(text: string): boolean {
|
|
|
21
22
|
return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** Maximum age (ms) for a message to receive a typing indicator reaction.
|
|
26
|
+
* Messages older than this are likely replays after context compaction (#30418). */
|
|
27
|
+
const TYPING_INDICATOR_MAX_AGE_MS = 2 * 60_000;
|
|
28
|
+
const MS_EPOCH_MIN = 1_000_000_000_000;
|
|
29
|
+
|
|
30
|
+
function normalizeEpochMs(timestamp: number | undefined): number | undefined {
|
|
31
|
+
if (!Number.isFinite(timestamp) || timestamp === undefined || timestamp <= 0) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
// Defensive normalization: some payloads use seconds, others milliseconds.
|
|
35
|
+
// Values below 1e12 are treated as epoch-seconds.
|
|
36
|
+
return timestamp < MS_EPOCH_MIN ? timestamp * 1000 : timestamp;
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
export type CreateFeishuReplyDispatcherParams = {
|
|
25
40
|
cfg: ClawdbotConfig;
|
|
26
41
|
agentId: string;
|
|
27
42
|
runtime: RuntimeEnv;
|
|
28
43
|
chatId: string;
|
|
29
44
|
replyToMessageId?: string;
|
|
45
|
+
/** When true, preserve typing indicator on reply target but send messages without reply metadata */
|
|
46
|
+
skipReplyToInMessages?: boolean;
|
|
47
|
+
replyInThread?: boolean;
|
|
48
|
+
rootId?: string;
|
|
30
49
|
mentionTargets?: MentionTarget[];
|
|
31
50
|
accountId?: string;
|
|
51
|
+
/** Epoch ms when the inbound message was created. Used to suppress typing
|
|
52
|
+
* indicators on old/replayed messages after context compaction (#30418). */
|
|
53
|
+
messageCreateTimeMs?: number;
|
|
32
54
|
};
|
|
33
55
|
|
|
34
56
|
export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) {
|
|
35
57
|
const core = getFeishuRuntime();
|
|
36
|
-
const {
|
|
58
|
+
const {
|
|
59
|
+
cfg,
|
|
60
|
+
agentId,
|
|
61
|
+
chatId,
|
|
62
|
+
replyToMessageId,
|
|
63
|
+
skipReplyToInMessages,
|
|
64
|
+
replyInThread,
|
|
65
|
+
rootId,
|
|
66
|
+
mentionTargets,
|
|
67
|
+
accountId,
|
|
68
|
+
} = params;
|
|
69
|
+
const sendReplyToMessageId = skipReplyToInMessages ? undefined : replyToMessageId;
|
|
37
70
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
38
71
|
const prefixContext = createReplyPrefixContext({ cfg, agentId });
|
|
39
72
|
|
|
40
73
|
let typingState: TypingIndicatorState | null = null;
|
|
41
74
|
const typingCallbacks = createTypingCallbacks({
|
|
42
75
|
start: async () => {
|
|
76
|
+
// Check if typing indicator is enabled (default: true)
|
|
77
|
+
if (!(account.config.typingIndicator ?? true)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
43
80
|
if (!replyToMessageId) {
|
|
44
81
|
return;
|
|
45
82
|
}
|
|
46
|
-
|
|
83
|
+
// Skip typing indicator for old messages — likely replays after context
|
|
84
|
+
// compaction that would flood users with stale notifications (#30418).
|
|
85
|
+
const messageCreateTimeMs = normalizeEpochMs(params.messageCreateTimeMs);
|
|
86
|
+
if (
|
|
87
|
+
messageCreateTimeMs !== undefined &&
|
|
88
|
+
Date.now() - messageCreateTimeMs > TYPING_INDICATOR_MAX_AGE_MS
|
|
89
|
+
) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
typingState = await addTypingIndicator({
|
|
93
|
+
cfg,
|
|
94
|
+
messageId: replyToMessageId,
|
|
95
|
+
accountId,
|
|
96
|
+
runtime: params.runtime,
|
|
97
|
+
});
|
|
47
98
|
},
|
|
48
99
|
stop: async () => {
|
|
49
100
|
if (!typingState) {
|
|
50
101
|
return;
|
|
51
102
|
}
|
|
52
|
-
await removeTypingIndicator({ cfg, state: typingState, accountId });
|
|
103
|
+
await removeTypingIndicator({ cfg, state: typingState, accountId, runtime: params.runtime });
|
|
53
104
|
typingState = null;
|
|
54
105
|
},
|
|
55
106
|
onStartError: (err) =>
|
|
@@ -99,7 +150,11 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
99
150
|
params.runtime.log?.(`feishu[${account.accountId}] ${message}`),
|
|
100
151
|
);
|
|
101
152
|
try {
|
|
102
|
-
await streaming.start(chatId, resolveReceiveIdType(chatId)
|
|
153
|
+
await streaming.start(chatId, resolveReceiveIdType(chatId), {
|
|
154
|
+
replyToMessageId,
|
|
155
|
+
replyInThread,
|
|
156
|
+
rootId,
|
|
157
|
+
});
|
|
103
158
|
} catch (error) {
|
|
104
159
|
params.runtime.error?.(`feishu: streaming start failed: ${String(error)}`);
|
|
105
160
|
streaming = null;
|
|
@@ -138,60 +193,99 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
138
193
|
},
|
|
139
194
|
deliver: async (payload: ReplyPayload, info) => {
|
|
140
195
|
const text = payload.text ?? "";
|
|
141
|
-
|
|
196
|
+
const mediaList =
|
|
197
|
+
payload.mediaUrls && payload.mediaUrls.length > 0
|
|
198
|
+
? payload.mediaUrls
|
|
199
|
+
: payload.mediaUrl
|
|
200
|
+
? [payload.mediaUrl]
|
|
201
|
+
: [];
|
|
202
|
+
const hasText = Boolean(text.trim());
|
|
203
|
+
const hasMedia = mediaList.length > 0;
|
|
204
|
+
|
|
205
|
+
if (!hasText && !hasMedia) {
|
|
142
206
|
return;
|
|
143
207
|
}
|
|
144
208
|
|
|
145
|
-
|
|
209
|
+
if (hasText) {
|
|
210
|
+
const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
|
|
146
211
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
212
|
+
if ((info?.kind === "block" || info?.kind === "final") && streamingEnabled && useCard) {
|
|
213
|
+
startStreaming();
|
|
214
|
+
if (streamingStartPromise) {
|
|
215
|
+
await streamingStartPromise;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (streaming?.isActive()) {
|
|
220
|
+
if (info?.kind === "final") {
|
|
221
|
+
streamText = text;
|
|
222
|
+
await closeStreaming();
|
|
223
|
+
}
|
|
224
|
+
// Send media even when streaming handled the text
|
|
225
|
+
if (hasMedia) {
|
|
226
|
+
for (const mediaUrl of mediaList) {
|
|
227
|
+
await sendMediaFeishu({
|
|
228
|
+
cfg,
|
|
229
|
+
to: chatId,
|
|
230
|
+
mediaUrl,
|
|
231
|
+
replyToMessageId: sendReplyToMessageId,
|
|
232
|
+
replyInThread,
|
|
233
|
+
accountId,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
151
238
|
}
|
|
152
|
-
}
|
|
153
239
|
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
240
|
+
let first = true;
|
|
241
|
+
if (useCard) {
|
|
242
|
+
for (const chunk of core.channel.text.chunkTextWithMode(
|
|
243
|
+
text,
|
|
244
|
+
textChunkLimit,
|
|
245
|
+
chunkMode,
|
|
246
|
+
)) {
|
|
247
|
+
await sendMarkdownCardFeishu({
|
|
248
|
+
cfg,
|
|
249
|
+
to: chatId,
|
|
250
|
+
text: chunk,
|
|
251
|
+
replyToMessageId: sendReplyToMessageId,
|
|
252
|
+
replyInThread,
|
|
253
|
+
mentions: first ? mentionTargets : undefined,
|
|
254
|
+
accountId,
|
|
255
|
+
});
|
|
256
|
+
first = false;
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const converted = core.channel.text.convertMarkdownTables(text, tableMode);
|
|
260
|
+
for (const chunk of core.channel.text.chunkTextWithMode(
|
|
261
|
+
converted,
|
|
262
|
+
textChunkLimit,
|
|
263
|
+
chunkMode,
|
|
264
|
+
)) {
|
|
265
|
+
await sendMessageFeishu({
|
|
266
|
+
cfg,
|
|
267
|
+
to: chatId,
|
|
268
|
+
text: chunk,
|
|
269
|
+
replyToMessageId: sendReplyToMessageId,
|
|
270
|
+
replyInThread,
|
|
271
|
+
mentions: first ? mentionTargets : undefined,
|
|
272
|
+
accountId,
|
|
273
|
+
});
|
|
274
|
+
first = false;
|
|
275
|
+
}
|
|
158
276
|
}
|
|
159
|
-
return;
|
|
160
277
|
}
|
|
161
278
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
text,
|
|
166
|
-
textChunkLimit,
|
|
167
|
-
chunkMode,
|
|
168
|
-
)) {
|
|
169
|
-
await sendMarkdownCardFeishu({
|
|
170
|
-
cfg,
|
|
171
|
-
to: chatId,
|
|
172
|
-
text: chunk,
|
|
173
|
-
replyToMessageId,
|
|
174
|
-
mentions: first ? mentionTargets : undefined,
|
|
175
|
-
accountId,
|
|
176
|
-
});
|
|
177
|
-
first = false;
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
const converted = core.channel.text.convertMarkdownTables(text, tableMode);
|
|
181
|
-
for (const chunk of core.channel.text.chunkTextWithMode(
|
|
182
|
-
converted,
|
|
183
|
-
textChunkLimit,
|
|
184
|
-
chunkMode,
|
|
185
|
-
)) {
|
|
186
|
-
await sendMessageFeishu({
|
|
279
|
+
if (hasMedia) {
|
|
280
|
+
for (const mediaUrl of mediaList) {
|
|
281
|
+
await sendMediaFeishu({
|
|
187
282
|
cfg,
|
|
188
283
|
to: chatId,
|
|
189
|
-
|
|
190
|
-
replyToMessageId,
|
|
191
|
-
|
|
284
|
+
mediaUrl,
|
|
285
|
+
replyToMessageId: sendReplyToMessageId,
|
|
286
|
+
replyInThread,
|
|
192
287
|
accountId,
|
|
193
288
|
});
|
|
194
|
-
first = false;
|
|
195
289
|
}
|
|
196
290
|
}
|
|
197
291
|
},
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const resolveFeishuSendTargetMock = vi.hoisted(() => vi.fn());
|
|
4
|
+
const resolveMarkdownTableModeMock = vi.hoisted(() => vi.fn(() => "preserve"));
|
|
5
|
+
const convertMarkdownTablesMock = vi.hoisted(() => vi.fn((text: string) => text));
|
|
6
|
+
|
|
7
|
+
vi.mock("./send-target.js", () => ({
|
|
8
|
+
resolveFeishuSendTarget: resolveFeishuSendTargetMock,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("./runtime.js", () => ({
|
|
12
|
+
getFeishuRuntime: () => ({
|
|
13
|
+
channel: {
|
|
14
|
+
text: {
|
|
15
|
+
resolveMarkdownTableMode: resolveMarkdownTableModeMock,
|
|
16
|
+
convertMarkdownTables: convertMarkdownTablesMock,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { sendCardFeishu, sendMessageFeishu } from "./send.js";
|
|
23
|
+
|
|
24
|
+
describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
25
|
+
const replyMock = vi.fn();
|
|
26
|
+
const createMock = vi.fn();
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
resolveFeishuSendTargetMock.mockReturnValue({
|
|
31
|
+
client: {
|
|
32
|
+
im: {
|
|
33
|
+
message: {
|
|
34
|
+
reply: replyMock,
|
|
35
|
+
create: createMock,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
receiveId: "ou_target",
|
|
40
|
+
receiveIdType: "open_id",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("falls back to create for withdrawn post replies", async () => {
|
|
45
|
+
replyMock.mockResolvedValue({
|
|
46
|
+
code: 230011,
|
|
47
|
+
msg: "The message was withdrawn.",
|
|
48
|
+
});
|
|
49
|
+
createMock.mockResolvedValue({
|
|
50
|
+
code: 0,
|
|
51
|
+
data: { message_id: "om_new" },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await sendMessageFeishu({
|
|
55
|
+
cfg: {} as never,
|
|
56
|
+
to: "user:ou_target",
|
|
57
|
+
text: "hello",
|
|
58
|
+
replyToMessageId: "om_parent",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(replyMock).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(result.messageId).toBe("om_new");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("falls back to create for withdrawn card replies", async () => {
|
|
67
|
+
replyMock.mockResolvedValue({
|
|
68
|
+
code: 231003,
|
|
69
|
+
msg: "The message is not found",
|
|
70
|
+
});
|
|
71
|
+
createMock.mockResolvedValue({
|
|
72
|
+
code: 0,
|
|
73
|
+
data: { message_id: "om_card_new" },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = await sendCardFeishu({
|
|
77
|
+
cfg: {} as never,
|
|
78
|
+
to: "user:ou_target",
|
|
79
|
+
card: { schema: "2.0" },
|
|
80
|
+
replyToMessageId: "om_parent",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(replyMock).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(result.messageId).toBe("om_card_new");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("still throws for non-withdrawn reply failures", async () => {
|
|
89
|
+
replyMock.mockResolvedValue({
|
|
90
|
+
code: 999999,
|
|
91
|
+
msg: "unknown failure",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await expect(
|
|
95
|
+
sendMessageFeishu({
|
|
96
|
+
cfg: {} as never,
|
|
97
|
+
to: "user:ou_target",
|
|
98
|
+
text: "hello",
|
|
99
|
+
replyToMessageId: "om_parent",
|
|
100
|
+
}),
|
|
101
|
+
).rejects.toThrow("Feishu reply failed");
|
|
102
|
+
|
|
103
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/send.test.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { getMessageFeishu } from "./send.js";
|
|
4
|
+
|
|
5
|
+
const { mockClientGet, mockCreateFeishuClient, mockResolveFeishuAccount } = vi.hoisted(() => ({
|
|
6
|
+
mockClientGet: vi.fn(),
|
|
7
|
+
mockCreateFeishuClient: vi.fn(),
|
|
8
|
+
mockResolveFeishuAccount: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("./client.js", () => ({
|
|
12
|
+
createFeishuClient: mockCreateFeishuClient,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("./accounts.js", () => ({
|
|
16
|
+
resolveFeishuAccount: mockResolveFeishuAccount,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe("getMessageFeishu", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
mockResolveFeishuAccount.mockReturnValue({
|
|
23
|
+
accountId: "default",
|
|
24
|
+
configured: true,
|
|
25
|
+
});
|
|
26
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
27
|
+
im: {
|
|
28
|
+
message: {
|
|
29
|
+
get: mockClientGet,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("extracts text content from interactive card elements", async () => {
|
|
36
|
+
mockClientGet.mockResolvedValueOnce({
|
|
37
|
+
code: 0,
|
|
38
|
+
data: {
|
|
39
|
+
items: [
|
|
40
|
+
{
|
|
41
|
+
message_id: "om_1",
|
|
42
|
+
chat_id: "oc_1",
|
|
43
|
+
msg_type: "interactive",
|
|
44
|
+
body: {
|
|
45
|
+
content: JSON.stringify({
|
|
46
|
+
elements: [
|
|
47
|
+
{ tag: "markdown", content: "hello markdown" },
|
|
48
|
+
{ tag: "div", text: { content: "hello div" } },
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const result = await getMessageFeishu({
|
|
58
|
+
cfg: {} as ClawdbotConfig,
|
|
59
|
+
messageId: "om_1",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual(
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
messageId: "om_1",
|
|
65
|
+
chatId: "oc_1",
|
|
66
|
+
contentType: "interactive",
|
|
67
|
+
content: "hello markdown\nhello div",
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("extracts text content from post messages", async () => {
|
|
73
|
+
mockClientGet.mockResolvedValueOnce({
|
|
74
|
+
code: 0,
|
|
75
|
+
data: {
|
|
76
|
+
items: [
|
|
77
|
+
{
|
|
78
|
+
message_id: "om_post",
|
|
79
|
+
chat_id: "oc_post",
|
|
80
|
+
msg_type: "post",
|
|
81
|
+
body: {
|
|
82
|
+
content: JSON.stringify({
|
|
83
|
+
zh_cn: {
|
|
84
|
+
title: "Summary",
|
|
85
|
+
content: [[{ tag: "text", text: "post body" }]],
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const result = await getMessageFeishu({
|
|
95
|
+
cfg: {} as ClawdbotConfig,
|
|
96
|
+
messageId: "om_post",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual(
|
|
100
|
+
expect.objectContaining({
|
|
101
|
+
messageId: "om_post",
|
|
102
|
+
chatId: "oc_post",
|
|
103
|
+
contentType: "post",
|
|
104
|
+
content: "Summary\n\npost body",
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns text placeholder instead of raw JSON for unsupported message types", async () => {
|
|
110
|
+
mockClientGet.mockResolvedValueOnce({
|
|
111
|
+
code: 0,
|
|
112
|
+
data: {
|
|
113
|
+
items: [
|
|
114
|
+
{
|
|
115
|
+
message_id: "om_file",
|
|
116
|
+
chat_id: "oc_file",
|
|
117
|
+
msg_type: "file",
|
|
118
|
+
body: {
|
|
119
|
+
content: JSON.stringify({ file_key: "file_v3_123" }),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const result = await getMessageFeishu({
|
|
127
|
+
cfg: {} as ClawdbotConfig,
|
|
128
|
+
messageId: "om_file",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result).toEqual(
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
messageId: "om_file",
|
|
134
|
+
chatId: "oc_file",
|
|
135
|
+
contentType: "file",
|
|
136
|
+
content: "[file message]",
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("supports single-object response shape from Feishu API", async () => {
|
|
142
|
+
mockClientGet.mockResolvedValueOnce({
|
|
143
|
+
code: 0,
|
|
144
|
+
data: {
|
|
145
|
+
message_id: "om_single",
|
|
146
|
+
chat_id: "oc_single",
|
|
147
|
+
msg_type: "text",
|
|
148
|
+
body: {
|
|
149
|
+
content: JSON.stringify({ text: "single payload" }),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await getMessageFeishu({
|
|
155
|
+
cfg: {} as ClawdbotConfig,
|
|
156
|
+
messageId: "om_single",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result).toEqual(
|
|
160
|
+
expect.objectContaining({
|
|
161
|
+
messageId: "om_single",
|
|
162
|
+
chatId: "oc_single",
|
|
163
|
+
contentType: "text",
|
|
164
|
+
content: "single payload",
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
});
|