@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.
- 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 +1653 -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 +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -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 +365 -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 +32 -94
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- 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 +375 -26
- package/src/media.ts +434 -88
- 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.reply-once.lifecycle.test-support.ts +190 -0
- 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 +77 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +399 -86
- 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 +479 -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
|
@@ -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
|
|
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
|
-
|
|
64
|
+
event.sender.sender_id = { user_id: "u_mobile_only" };
|
|
65
65
|
|
|
66
|
-
const ctx = parseFeishuMessageEvent(event
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
)
|
|
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
|
-
)
|
|
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
|
-
])
|
|
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: {} }])
|
|
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" } }])
|
|
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
|
-
])
|
|
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"><script></at> test');
|