@nextclaw/channel-plugin-feishu 0.2.12 → 0.2.14
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/README.md +3 -1
- package/index.ts +65 -0
- package/openclaw.plugin.json +3 -7
- package/package.json +33 -9
- package/skills/feishu-doc/SKILL.md +211 -0
- package/skills/feishu-doc/references/block-types.md +103 -0
- package/skills/feishu-drive/SKILL.md +97 -0
- package/skills/feishu-perm/SKILL.md +119 -0
- package/skills/feishu-wiki/SKILL.md +111 -0
- package/src/accounts.test.ts +371 -0
- package/src/accounts.ts +244 -0
- package/src/async.ts +62 -0
- package/src/bitable.ts +725 -0
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +193 -0
- package/src/bot.stripBotMention.test.ts +134 -0
- package/src/bot.test.ts +2107 -0
- package/src/bot.ts +1556 -0
- package/src/card-action.ts +79 -0
- package/src/channel.test.ts +48 -0
- package/src/channel.ts +369 -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 +324 -0
- package/src/client.ts +196 -0
- package/src/config-schema.test.ts +247 -0
- package/src/config-schema.ts +306 -0
- package/src/dedup.ts +203 -0
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +156 -0
- package/src/doc-schema.ts +182 -0
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +187 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +70 -0
- package/src/docx.test.ts +445 -0
- package/src/docx.ts +1460 -0
- package/src/drive-schema.ts +46 -0
- package/src/drive.ts +228 -0
- package/src/dynamic-agent.ts +131 -0
- package/src/external-keys.test.ts +20 -0
- package/src/external-keys.ts +19 -0
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +523 -0
- package/src/media.ts +484 -0
- package/src/mention.ts +133 -0
- package/src/monitor.account.ts +562 -0
- package/src/monitor.reaction.test.ts +653 -0
- package/src/monitor.startup.test.ts +190 -0
- package/src/monitor.startup.ts +64 -0
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +155 -0
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +264 -0
- package/src/monitor.ts +95 -0
- package/src/monitor.webhook-e2e.test.ts +214 -0
- package/src/monitor.webhook-security.test.ts +142 -0
- package/src/monitor.webhook.test-helpers.ts +98 -0
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +489 -0
- package/src/outbound.test.ts +356 -0
- package/src/outbound.ts +176 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +176 -0
- package/src/policy.test.ts +154 -0
- package/src/policy.ts +123 -0
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +270 -0
- package/src/probe.ts +156 -0
- package/src/reactions.ts +153 -0
- package/src/reply-dispatcher.test.ts +513 -0
- package/src/reply-dispatcher.ts +397 -0
- package/src/runtime.ts +6 -0
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-result.ts +29 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +29 -0
- package/src/send.reply-fallback.test.ts +189 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +481 -0
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +374 -0
- package/src/targets.test.ts +70 -0
- package/src/targets.ts +107 -0
- 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/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +103 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +210 -0
- package/src/wiki-schema.ts +55 -0
- package/src/wiki.ts +233 -0
- package/index.js +0 -27
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
|
|
3
|
+
|
|
4
|
+
// Mock resolveFeishuAccount
|
|
5
|
+
vi.mock("./accounts.js", () => ({
|
|
6
|
+
resolveFeishuAccount: vi.fn().mockReturnValue({ accountId: "mock-account" }),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
// Mock bot.js to verify handleFeishuMessage call
|
|
10
|
+
vi.mock("./bot.js", () => ({
|
|
11
|
+
handleFeishuMessage: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import { handleFeishuMessage } from "./bot.js";
|
|
15
|
+
|
|
16
|
+
describe("Feishu Card Action Handler", () => {
|
|
17
|
+
const cfg = {} as any; // Minimal mock
|
|
18
|
+
const runtime = { log: vi.fn(), error: vi.fn() } as any;
|
|
19
|
+
|
|
20
|
+
it("handles card action with text payload", async () => {
|
|
21
|
+
const event: FeishuCardActionEvent = {
|
|
22
|
+
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
|
23
|
+
token: "tok1",
|
|
24
|
+
action: { value: { text: "/ping" }, tag: "button" },
|
|
25
|
+
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
await handleFeishuCardAction({ cfg, event, runtime });
|
|
29
|
+
|
|
30
|
+
expect(handleFeishuMessage).toHaveBeenCalledWith(
|
|
31
|
+
expect.objectContaining({
|
|
32
|
+
event: expect.objectContaining({
|
|
33
|
+
message: expect.objectContaining({
|
|
34
|
+
content: '{"text":"/ping"}',
|
|
35
|
+
chat_id: "chat1",
|
|
36
|
+
}),
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("handles card action with JSON object payload", async () => {
|
|
43
|
+
const event: FeishuCardActionEvent = {
|
|
44
|
+
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
|
45
|
+
token: "tok2",
|
|
46
|
+
action: { value: { key: "val" }, tag: "button" },
|
|
47
|
+
context: { open_id: "u123", user_id: "uid1", chat_id: "" },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await handleFeishuCardAction({ cfg, event, runtime });
|
|
51
|
+
|
|
52
|
+
expect(handleFeishuMessage).toHaveBeenCalledWith(
|
|
53
|
+
expect.objectContaining({
|
|
54
|
+
event: expect.objectContaining({
|
|
55
|
+
message: expect.objectContaining({
|
|
56
|
+
content: '{"text":"{\\"key\\":\\"val\\"}"}',
|
|
57
|
+
chat_id: "u123", // Fallback to open_id
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseFeishuMessageEvent } from "./bot.js";
|
|
3
|
+
|
|
4
|
+
// Helper to build a minimal FeishuMessageEvent for testing
|
|
5
|
+
function makeEvent(
|
|
6
|
+
chatType: "p2p" | "group" | "private",
|
|
7
|
+
mentions?: Array<{ key: string; name: string; id: { open_id?: string } }>,
|
|
8
|
+
text = "hello",
|
|
9
|
+
) {
|
|
10
|
+
return {
|
|
11
|
+
sender: {
|
|
12
|
+
sender_id: { user_id: "u1", open_id: "ou_sender" },
|
|
13
|
+
},
|
|
14
|
+
message: {
|
|
15
|
+
message_id: "msg_1",
|
|
16
|
+
chat_id: "oc_chat1",
|
|
17
|
+
chat_type: chatType,
|
|
18
|
+
message_type: "text",
|
|
19
|
+
content: JSON.stringify({ text }),
|
|
20
|
+
mentions,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makePostEvent(content: unknown) {
|
|
26
|
+
return {
|
|
27
|
+
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
|
|
28
|
+
message: {
|
|
29
|
+
message_id: "msg_1",
|
|
30
|
+
chat_id: "oc_chat1",
|
|
31
|
+
chat_type: "group",
|
|
32
|
+
message_type: "post",
|
|
33
|
+
content: JSON.stringify(content),
|
|
34
|
+
mentions: [],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeShareChatEvent(content: unknown) {
|
|
40
|
+
return {
|
|
41
|
+
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
|
|
42
|
+
message: {
|
|
43
|
+
message_id: "msg_1",
|
|
44
|
+
chat_id: "oc_chat1",
|
|
45
|
+
chat_type: "group",
|
|
46
|
+
message_type: "share_chat",
|
|
47
|
+
content: JSON.stringify(content),
|
|
48
|
+
mentions: [],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("parseFeishuMessageEvent – mentionedBot", () => {
|
|
54
|
+
const BOT_OPEN_ID = "ou_bot_123";
|
|
55
|
+
|
|
56
|
+
it("returns mentionedBot=false when there are no mentions", () => {
|
|
57
|
+
const event = makeEvent("group", []);
|
|
58
|
+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
|
|
59
|
+
expect(ctx.mentionedBot).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("falls back to sender user_id when open_id is missing", () => {
|
|
63
|
+
const event = makeEvent("p2p", []);
|
|
64
|
+
(event as any).sender.sender_id = { user_id: "u_mobile_only" };
|
|
65
|
+
|
|
66
|
+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
|
|
67
|
+
expect(ctx.senderOpenId).toBe("u_mobile_only");
|
|
68
|
+
expect(ctx.senderId).toBe("u_mobile_only");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns mentionedBot=true when bot is mentioned", () => {
|
|
72
|
+
const event = makeEvent("group", [
|
|
73
|
+
{ key: "@_user_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },
|
|
74
|
+
]);
|
|
75
|
+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
|
|
76
|
+
expect(ctx.mentionedBot).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns mentionedBot=true when bot mention name differs from configured botName", () => {
|
|
80
|
+
const event = makeEvent("group", [
|
|
81
|
+
{ key: "@_user_1", name: "OpenClaw Bot (Alias)", id: { open_id: BOT_OPEN_ID } },
|
|
82
|
+
]);
|
|
83
|
+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID, "OpenClaw Bot");
|
|
84
|
+
expect(ctx.mentionedBot).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns mentionedBot=false when only other users are mentioned", () => {
|
|
88
|
+
const event = makeEvent("group", [
|
|
89
|
+
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
|
|
90
|
+
]);
|
|
91
|
+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
|
|
92
|
+
expect(ctx.mentionedBot).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns mentionedBot=false when botOpenId is undefined (unknown bot)", () => {
|
|
96
|
+
const event = makeEvent("group", [
|
|
97
|
+
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
|
|
98
|
+
]);
|
|
99
|
+
const ctx = parseFeishuMessageEvent(event as any, undefined);
|
|
100
|
+
expect(ctx.mentionedBot).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns mentionedBot=false when botOpenId is empty string (probe failed)", () => {
|
|
104
|
+
const event = makeEvent("group", [
|
|
105
|
+
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
|
|
106
|
+
]);
|
|
107
|
+
const ctx = parseFeishuMessageEvent(event as any, "");
|
|
108
|
+
expect(ctx.mentionedBot).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("treats mention.name regex metacharacters as literals when stripping", () => {
|
|
112
|
+
const event = makeEvent(
|
|
113
|
+
"group",
|
|
114
|
+
[{ key: "@_bot_1", name: ".*", id: { open_id: BOT_OPEN_ID } }],
|
|
115
|
+
"@NotBot hello",
|
|
116
|
+
);
|
|
117
|
+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
|
|
118
|
+
expect(ctx.content).toBe("@NotBot hello");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("treats mention.key regex metacharacters as literals when stripping", () => {
|
|
122
|
+
const event = makeEvent(
|
|
123
|
+
"group",
|
|
124
|
+
[{ key: ".*", name: "Bot", id: { open_id: BOT_OPEN_ID } }],
|
|
125
|
+
"hello world",
|
|
126
|
+
);
|
|
127
|
+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
|
|
128
|
+
expect(ctx.content).toBe("hello world");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns mentionedBot=true for post message with at (no top-level mentions)", () => {
|
|
132
|
+
const BOT_OPEN_ID = "ou_bot_123";
|
|
133
|
+
const event = makePostEvent({
|
|
134
|
+
content: [
|
|
135
|
+
[{ tag: "at", user_id: BOT_OPEN_ID, user_name: "claw" }],
|
|
136
|
+
[{ tag: "text", text: "What does this document say" }],
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
|
|
140
|
+
expect(ctx.mentionedBot).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("returns mentionedBot=false for post message with no at", () => {
|
|
144
|
+
const event = makePostEvent({
|
|
145
|
+
content: [[{ tag: "text", text: "hello" }]],
|
|
146
|
+
});
|
|
147
|
+
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
|
|
148
|
+
expect(ctx.mentionedBot).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns mentionedBot=false for post message with at for another user", () => {
|
|
152
|
+
const event = makePostEvent({
|
|
153
|
+
content: [
|
|
154
|
+
[{ tag: "at", user_id: "ou_other", user_name: "other" }],
|
|
155
|
+
[{ tag: "text", text: "hello" }],
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
|
|
159
|
+
expect(ctx.mentionedBot).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("preserves post code and code_block content", () => {
|
|
163
|
+
const event = makePostEvent({
|
|
164
|
+
content: [
|
|
165
|
+
[
|
|
166
|
+
{ tag: "text", text: "before " },
|
|
167
|
+
{ tag: "code", text: "inline()" },
|
|
168
|
+
],
|
|
169
|
+
[{ tag: "code_block", language: "ts", text: "const x = 1;" }],
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
|
|
173
|
+
expect(ctx.content).toContain("before `inline()`");
|
|
174
|
+
expect(ctx.content).toContain("```ts\nconst x = 1;\n```");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("uses share_chat body when available", () => {
|
|
178
|
+
const event = makeShareChatEvent({
|
|
179
|
+
body: "Merged and Forwarded Message",
|
|
180
|
+
share_chat_id: "sc_abc123",
|
|
181
|
+
});
|
|
182
|
+
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
|
|
183
|
+
expect(ctx.content).toBe("Merged and Forwarded Message");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("falls back to share_chat identifier when body is unavailable", () => {
|
|
187
|
+
const event = makeShareChatEvent({
|
|
188
|
+
share_chat_id: "sc_abc123",
|
|
189
|
+
});
|
|
190
|
+
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
|
|
191
|
+
expect(ctx.content).toBe("[Forwarded message: sc_abc123]");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseFeishuMessageEvent } from "./bot.js";
|
|
3
|
+
|
|
4
|
+
function makeEvent(
|
|
5
|
+
text: string,
|
|
6
|
+
mentions?: Array<{ key: string; name: string; id: { open_id?: string; user_id?: string } }>,
|
|
7
|
+
chatType: "p2p" | "group" = "p2p",
|
|
8
|
+
) {
|
|
9
|
+
return {
|
|
10
|
+
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
|
|
11
|
+
message: {
|
|
12
|
+
message_id: "msg_1",
|
|
13
|
+
chat_id: "oc_chat1",
|
|
14
|
+
chat_type: chatType,
|
|
15
|
+
message_type: "text",
|
|
16
|
+
content: JSON.stringify({ text }),
|
|
17
|
+
mentions,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BOT_OPEN_ID = "ou_bot";
|
|
23
|
+
|
|
24
|
+
describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
|
|
25
|
+
it("returns original text when mentions are missing", () => {
|
|
26
|
+
const ctx = parseFeishuMessageEvent(makeEvent("hello world", undefined) as any, BOT_OPEN_ID);
|
|
27
|
+
expect(ctx.content).toBe("hello world");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("strips bot mention in p2p (addressing prefix, not semantic content)", () => {
|
|
31
|
+
const ctx = parseFeishuMessageEvent(
|
|
32
|
+
makeEvent("@_bot_1 hello", [
|
|
33
|
+
{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } },
|
|
34
|
+
]) as any,
|
|
35
|
+
BOT_OPEN_ID,
|
|
36
|
+
);
|
|
37
|
+
expect(ctx.content).toBe("hello");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("strips bot mention in group so slash commands work (#35994)", () => {
|
|
41
|
+
const ctx = parseFeishuMessageEvent(
|
|
42
|
+
makeEvent(
|
|
43
|
+
"@_bot_1 hello",
|
|
44
|
+
[{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }],
|
|
45
|
+
"group",
|
|
46
|
+
) as any,
|
|
47
|
+
BOT_OPEN_ID,
|
|
48
|
+
);
|
|
49
|
+
expect(ctx.content).toBe("hello");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("strips bot mention in group preserving slash command prefix (#35994)", () => {
|
|
53
|
+
const ctx = parseFeishuMessageEvent(
|
|
54
|
+
makeEvent(
|
|
55
|
+
"@_bot_1 /model",
|
|
56
|
+
[{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }],
|
|
57
|
+
"group",
|
|
58
|
+
) as any,
|
|
59
|
+
BOT_OPEN_ID,
|
|
60
|
+
);
|
|
61
|
+
expect(ctx.content).toBe("/model");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("strips bot mention but normalizes other mentions in p2p (mention-forward)", () => {
|
|
65
|
+
const ctx = parseFeishuMessageEvent(
|
|
66
|
+
makeEvent("@_bot_1 @_user_alice hello", [
|
|
67
|
+
{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } },
|
|
68
|
+
{ key: "@_user_alice", name: "Alice", id: { open_id: "ou_alice" } },
|
|
69
|
+
]) as any,
|
|
70
|
+
BOT_OPEN_ID,
|
|
71
|
+
);
|
|
72
|
+
expect(ctx.content).toBe('<at user_id="ou_alice">Alice</at> hello');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("falls back to @name when open_id is absent", () => {
|
|
76
|
+
const ctx = parseFeishuMessageEvent(
|
|
77
|
+
makeEvent("@_user_1 hi", [
|
|
78
|
+
{ key: "@_user_1", name: "Alice", id: { user_id: "uid_alice" } },
|
|
79
|
+
]) as any,
|
|
80
|
+
BOT_OPEN_ID,
|
|
81
|
+
);
|
|
82
|
+
expect(ctx.content).toBe("@Alice hi");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("falls back to plain @name when no id is present", () => {
|
|
86
|
+
const ctx = parseFeishuMessageEvent(
|
|
87
|
+
makeEvent("@_unknown hey", [{ key: "@_unknown", name: "Nobody", id: {} }]) as any,
|
|
88
|
+
BOT_OPEN_ID,
|
|
89
|
+
);
|
|
90
|
+
expect(ctx.content).toBe("@Nobody hey");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("treats mention key regex metacharacters as literal text", () => {
|
|
94
|
+
const ctx = parseFeishuMessageEvent(
|
|
95
|
+
makeEvent("hello world", [{ key: ".*", name: "Bot", id: { open_id: "ou_bot" } }]) as any,
|
|
96
|
+
BOT_OPEN_ID,
|
|
97
|
+
);
|
|
98
|
+
expect(ctx.content).toBe("hello world");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("normalizes multiple mentions in one pass", () => {
|
|
102
|
+
const ctx = parseFeishuMessageEvent(
|
|
103
|
+
makeEvent("@_bot_1 hi @_user_2", [
|
|
104
|
+
{ key: "@_bot_1", name: "Bot One", id: { open_id: "ou_bot_1" } },
|
|
105
|
+
{ key: "@_user_2", name: "User Two", id: { open_id: "ou_user_2" } },
|
|
106
|
+
]) as any,
|
|
107
|
+
BOT_OPEN_ID,
|
|
108
|
+
);
|
|
109
|
+
expect(ctx.content).toBe(
|
|
110
|
+
'<at user_id="ou_bot_1">Bot One</at> hi <at user_id="ou_user_2">User Two</at>',
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("treats $ in display name as literal (no replacement-pattern interpolation)", () => {
|
|
115
|
+
const ctx = parseFeishuMessageEvent(
|
|
116
|
+
makeEvent("@_user_1 hi", [
|
|
117
|
+
{ key: "@_user_1", name: "$& the user", id: { open_id: "ou_x" } },
|
|
118
|
+
]) as any,
|
|
119
|
+
BOT_OPEN_ID,
|
|
120
|
+
);
|
|
121
|
+
// $ is preserved literally (no $& pattern substitution); & is not escaped in tag body
|
|
122
|
+
expect(ctx.content).toBe('<at user_id="ou_x">$& the user</at> hi');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("escapes < and > in mention name to protect tag structure", () => {
|
|
126
|
+
const ctx = parseFeishuMessageEvent(
|
|
127
|
+
makeEvent("@_user_1 test", [
|
|
128
|
+
{ key: "@_user_1", name: "<script>", id: { open_id: "ou_x" } },
|
|
129
|
+
]) as any,
|
|
130
|
+
BOT_OPEN_ID,
|
|
131
|
+
);
|
|
132
|
+
expect(ctx.content).toBe('<at user_id="ou_x"><script></at> test');
|
|
133
|
+
});
|
|
134
|
+
});
|