@openclaw/feishu 2026.3.2 → 2026.3.7
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 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +199 -13
- package/src/accounts.ts +45 -17
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +8 -0
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +516 -9
- package/src/bot.ts +366 -109
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +52 -64
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +207 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +14 -6
- package/src/config-schema.ts +5 -1
- package/src/dedup.ts +1 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +3 -3
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +60 -13
- package/src/media.ts +23 -9
- package/src/monitor.account.ts +19 -8
- package/src/monitor.reaction.test.ts +111 -105
- package/src/monitor.startup.test.ts +11 -10
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.ts +4 -1
- package/src/monitor.test-mocks.ts +42 -9
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +8 -23
- package/src/onboarding.status.test.ts +1 -1
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +86 -71
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +18 -18
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +175 -0
- package/src/reply-dispatcher.ts +69 -21
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +8 -14
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +1 -1
- package/src/send-target.ts +1 -1
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.ts +5 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +2 -3
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
package/src/outbound.test.ts
CHANGED
|
@@ -136,6 +136,156 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
136
136
|
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
137
137
|
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "card_msg" }));
|
|
138
138
|
});
|
|
139
|
+
|
|
140
|
+
it("forwards replyToId as replyToMessageId on sendText", async () => {
|
|
141
|
+
await sendText({
|
|
142
|
+
cfg: {} as any,
|
|
143
|
+
to: "chat_1",
|
|
144
|
+
text: "hello",
|
|
145
|
+
replyToId: "om_reply_1",
|
|
146
|
+
accountId: "main",
|
|
147
|
+
} as any);
|
|
148
|
+
|
|
149
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
to: "chat_1",
|
|
152
|
+
text: "hello",
|
|
153
|
+
replyToMessageId: "om_reply_1",
|
|
154
|
+
accountId: "main",
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("falls back to threadId when replyToId is empty on sendText", async () => {
|
|
160
|
+
await sendText({
|
|
161
|
+
cfg: {} as any,
|
|
162
|
+
to: "chat_1",
|
|
163
|
+
text: "hello",
|
|
164
|
+
replyToId: " ",
|
|
165
|
+
threadId: "om_thread_2",
|
|
166
|
+
accountId: "main",
|
|
167
|
+
} as any);
|
|
168
|
+
|
|
169
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
170
|
+
expect.objectContaining({
|
|
171
|
+
to: "chat_1",
|
|
172
|
+
text: "hello",
|
|
173
|
+
replyToMessageId: "om_thread_2",
|
|
174
|
+
accountId: "main",
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
vi.clearAllMocks();
|
|
183
|
+
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
184
|
+
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
185
|
+
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("forwards replyToId as replyToMessageId to sendMessageFeishu", async () => {
|
|
189
|
+
await sendText({
|
|
190
|
+
cfg: {} as any,
|
|
191
|
+
to: "chat_1",
|
|
192
|
+
text: "hello",
|
|
193
|
+
replyToId: "om_reply_target",
|
|
194
|
+
accountId: "main",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
198
|
+
expect.objectContaining({
|
|
199
|
+
to: "chat_1",
|
|
200
|
+
text: "hello",
|
|
201
|
+
replyToMessageId: "om_reply_target",
|
|
202
|
+
accountId: "main",
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("forwards replyToId to sendMarkdownCardFeishu when renderMode=card", async () => {
|
|
208
|
+
await sendText({
|
|
209
|
+
cfg: {
|
|
210
|
+
channels: {
|
|
211
|
+
feishu: {
|
|
212
|
+
renderMode: "card",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
} as any,
|
|
216
|
+
to: "chat_1",
|
|
217
|
+
text: "```code```",
|
|
218
|
+
replyToId: "om_reply_target",
|
|
219
|
+
accountId: "main",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(sendMarkdownCardFeishuMock).toHaveBeenCalledWith(
|
|
223
|
+
expect.objectContaining({
|
|
224
|
+
replyToMessageId: "om_reply_target",
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("does not pass replyToMessageId when replyToId is absent", async () => {
|
|
230
|
+
await sendText({
|
|
231
|
+
cfg: {} as any,
|
|
232
|
+
to: "chat_1",
|
|
233
|
+
text: "hello",
|
|
234
|
+
accountId: "main",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
to: "chat_1",
|
|
240
|
+
text: "hello",
|
|
241
|
+
accountId: "main",
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
expect(sendMessageFeishuMock.mock.calls[0][0].replyToMessageId).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
|
249
|
+
beforeEach(() => {
|
|
250
|
+
vi.clearAllMocks();
|
|
251
|
+
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
252
|
+
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
253
|
+
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("forwards replyToId to sendMediaFeishu", async () => {
|
|
257
|
+
await feishuOutbound.sendMedia?.({
|
|
258
|
+
cfg: {} as any,
|
|
259
|
+
to: "chat_1",
|
|
260
|
+
text: "",
|
|
261
|
+
mediaUrl: "https://example.com/image.png",
|
|
262
|
+
replyToId: "om_reply_target",
|
|
263
|
+
accountId: "main",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
267
|
+
expect.objectContaining({
|
|
268
|
+
replyToMessageId: "om_reply_target",
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("forwards replyToId to text caption send", async () => {
|
|
274
|
+
await feishuOutbound.sendMedia?.({
|
|
275
|
+
cfg: {} as any,
|
|
276
|
+
to: "chat_1",
|
|
277
|
+
text: "caption text",
|
|
278
|
+
mediaUrl: "https://example.com/image.png",
|
|
279
|
+
replyToId: "om_reply_target",
|
|
280
|
+
accountId: "main",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
284
|
+
expect.objectContaining({
|
|
285
|
+
replyToMessageId: "om_reply_target",
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
139
289
|
});
|
|
140
290
|
|
|
141
291
|
describe("feishuOutbound.sendMedia renderMode", () => {
|
|
@@ -178,4 +328,32 @@ describe("feishuOutbound.sendMedia renderMode", () => {
|
|
|
178
328
|
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
179
329
|
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "media_msg" }));
|
|
180
330
|
});
|
|
331
|
+
|
|
332
|
+
it("uses threadId fallback as replyToMessageId on sendMedia", async () => {
|
|
333
|
+
await feishuOutbound.sendMedia?.({
|
|
334
|
+
cfg: {} as any,
|
|
335
|
+
to: "chat_1",
|
|
336
|
+
text: "caption",
|
|
337
|
+
mediaUrl: "https://example.com/image.png",
|
|
338
|
+
threadId: "om_thread_1",
|
|
339
|
+
accountId: "main",
|
|
340
|
+
} as any);
|
|
341
|
+
|
|
342
|
+
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
343
|
+
expect.objectContaining({
|
|
344
|
+
to: "chat_1",
|
|
345
|
+
mediaUrl: "https://example.com/image.png",
|
|
346
|
+
replyToMessageId: "om_thread_1",
|
|
347
|
+
accountId: "main",
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
351
|
+
expect.objectContaining({
|
|
352
|
+
to: "chat_1",
|
|
353
|
+
text: "caption",
|
|
354
|
+
replyToMessageId: "om_thread_1",
|
|
355
|
+
accountId: "main",
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
});
|
|
181
359
|
});
|
package/src/outbound.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/feishu";
|
|
4
4
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
5
5
|
import { sendMediaFeishu } from "./media.js";
|
|
6
6
|
import { getFeishuRuntime } from "./runtime.js";
|
|
@@ -43,21 +43,37 @@ function shouldUseCard(text: string): boolean {
|
|
|
43
43
|
return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function resolveReplyToMessageId(params: {
|
|
47
|
+
replyToId?: string | null;
|
|
48
|
+
threadId?: string | number | null;
|
|
49
|
+
}): string | undefined {
|
|
50
|
+
const replyToId = params.replyToId?.trim();
|
|
51
|
+
if (replyToId) {
|
|
52
|
+
return replyToId;
|
|
53
|
+
}
|
|
54
|
+
if (params.threadId == null) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const trimmed = String(params.threadId).trim();
|
|
58
|
+
return trimmed || undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
async function sendOutboundText(params: {
|
|
47
62
|
cfg: Parameters<typeof sendMessageFeishu>[0]["cfg"];
|
|
48
63
|
to: string;
|
|
49
64
|
text: string;
|
|
65
|
+
replyToMessageId?: string;
|
|
50
66
|
accountId?: string;
|
|
51
67
|
}) {
|
|
52
|
-
const { cfg, to, text, accountId } = params;
|
|
68
|
+
const { cfg, to, text, accountId, replyToMessageId } = params;
|
|
53
69
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
54
70
|
const renderMode = account.config?.renderMode ?? "auto";
|
|
55
71
|
|
|
56
72
|
if (renderMode === "card" || (renderMode === "auto" && shouldUseCard(text))) {
|
|
57
|
-
return sendMarkdownCardFeishu({ cfg, to, text, accountId });
|
|
73
|
+
return sendMarkdownCardFeishu({ cfg, to, text, accountId, replyToMessageId });
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
return sendMessageFeishu({ cfg, to, text, accountId });
|
|
76
|
+
return sendMessageFeishu({ cfg, to, text, accountId, replyToMessageId });
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
export const feishuOutbound: ChannelOutboundAdapter = {
|
|
@@ -65,7 +81,8 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
65
81
|
chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
66
82
|
chunkerMode: "markdown",
|
|
67
83
|
textChunkLimit: 4000,
|
|
68
|
-
sendText: async ({ cfg, to, text, accountId }) => {
|
|
84
|
+
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
|
85
|
+
const replyToMessageId = resolveReplyToMessageId({ replyToId, threadId });
|
|
69
86
|
// Scheme A compatibility shim:
|
|
70
87
|
// when upstream accidentally returns a local image path as plain text,
|
|
71
88
|
// auto-upload and send as Feishu image message instead of leaking path text.
|
|
@@ -77,6 +94,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
77
94
|
to,
|
|
78
95
|
mediaUrl: localImagePath,
|
|
79
96
|
accountId: accountId ?? undefined,
|
|
97
|
+
replyToMessageId,
|
|
80
98
|
});
|
|
81
99
|
return { channel: "feishu", ...result };
|
|
82
100
|
} catch (err) {
|
|
@@ -90,10 +108,21 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
90
108
|
to,
|
|
91
109
|
text,
|
|
92
110
|
accountId: accountId ?? undefined,
|
|
111
|
+
replyToMessageId,
|
|
93
112
|
});
|
|
94
113
|
return { channel: "feishu", ...result };
|
|
95
114
|
},
|
|
96
|
-
sendMedia: async ({
|
|
115
|
+
sendMedia: async ({
|
|
116
|
+
cfg,
|
|
117
|
+
to,
|
|
118
|
+
text,
|
|
119
|
+
mediaUrl,
|
|
120
|
+
accountId,
|
|
121
|
+
mediaLocalRoots,
|
|
122
|
+
replyToId,
|
|
123
|
+
threadId,
|
|
124
|
+
}) => {
|
|
125
|
+
const replyToMessageId = resolveReplyToMessageId({ replyToId, threadId });
|
|
97
126
|
// Send text first if provided
|
|
98
127
|
if (text?.trim()) {
|
|
99
128
|
await sendOutboundText({
|
|
@@ -101,6 +130,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
101
130
|
to,
|
|
102
131
|
text,
|
|
103
132
|
accountId: accountId ?? undefined,
|
|
133
|
+
replyToMessageId,
|
|
104
134
|
});
|
|
105
135
|
}
|
|
106
136
|
|
|
@@ -113,6 +143,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
113
143
|
mediaUrl,
|
|
114
144
|
accountId: accountId ?? undefined,
|
|
115
145
|
mediaLocalRoots,
|
|
146
|
+
replyToMessageId,
|
|
116
147
|
});
|
|
117
148
|
return { channel: "feishu", ...result };
|
|
118
149
|
} catch (err) {
|
|
@@ -125,6 +156,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
125
156
|
to,
|
|
126
157
|
text: fallbackText,
|
|
127
158
|
accountId: accountId ?? undefined,
|
|
159
|
+
replyToMessageId,
|
|
128
160
|
});
|
|
129
161
|
return { channel: "feishu", ...result };
|
|
130
162
|
}
|
|
@@ -136,6 +168,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
136
168
|
to,
|
|
137
169
|
text: text ?? "",
|
|
138
170
|
accountId: accountId ?? undefined,
|
|
171
|
+
replyToMessageId,
|
|
139
172
|
});
|
|
140
173
|
return { channel: "feishu", ...result };
|
|
141
174
|
},
|
package/src/perm.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
3
3
|
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
4
|
import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
|
|
5
5
|
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
12
|
-
details: data,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
6
|
+
import {
|
|
7
|
+
jsonToolResult,
|
|
8
|
+
toolExecutionErrorResult,
|
|
9
|
+
unknownToolActionResult,
|
|
10
|
+
} from "./tool-result.js";
|
|
15
11
|
|
|
16
12
|
type ListTokenType =
|
|
17
13
|
| "doc"
|
|
@@ -154,21 +150,21 @@ export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
|
|
154
150
|
});
|
|
155
151
|
switch (p.action) {
|
|
156
152
|
case "list":
|
|
157
|
-
return
|
|
153
|
+
return jsonToolResult(await listMembers(client, p.token, p.type));
|
|
158
154
|
case "add":
|
|
159
|
-
return
|
|
155
|
+
return jsonToolResult(
|
|
160
156
|
await addMember(client, p.token, p.type, p.member_type, p.member_id, p.perm),
|
|
161
157
|
);
|
|
162
158
|
case "remove":
|
|
163
|
-
return
|
|
159
|
+
return jsonToolResult(
|
|
164
160
|
await removeMember(client, p.token, p.type, p.member_type, p.member_id),
|
|
165
161
|
);
|
|
166
162
|
default:
|
|
167
163
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
|
|
168
|
-
return
|
|
164
|
+
return unknownToolActionResult((p as { action?: unknown }).action);
|
|
169
165
|
}
|
|
170
166
|
} catch (err) {
|
|
171
|
-
return
|
|
167
|
+
return toolExecutionErrorResult(err);
|
|
172
168
|
}
|
|
173
169
|
},
|
|
174
170
|
};
|
package/src/policy.test.ts
CHANGED
|
@@ -110,5 +110,45 @@ describe("feishu policy", () => {
|
|
|
110
110
|
}),
|
|
111
111
|
).toBe(true);
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
it("allows group when groupPolicy is 'open'", () => {
|
|
115
|
+
expect(
|
|
116
|
+
isFeishuGroupAllowed({
|
|
117
|
+
groupPolicy: "open",
|
|
118
|
+
allowFrom: [],
|
|
119
|
+
senderId: "oc_group_999",
|
|
120
|
+
}),
|
|
121
|
+
).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("treats 'allowall' as equivalent to 'open'", () => {
|
|
125
|
+
expect(
|
|
126
|
+
isFeishuGroupAllowed({
|
|
127
|
+
groupPolicy: "allowall",
|
|
128
|
+
allowFrom: [],
|
|
129
|
+
senderId: "oc_group_999",
|
|
130
|
+
}),
|
|
131
|
+
).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("rejects group when groupPolicy is 'disabled'", () => {
|
|
135
|
+
expect(
|
|
136
|
+
isFeishuGroupAllowed({
|
|
137
|
+
groupPolicy: "disabled",
|
|
138
|
+
allowFrom: ["oc_group_999"],
|
|
139
|
+
senderId: "oc_group_999",
|
|
140
|
+
}),
|
|
141
|
+
).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("rejects group when groupPolicy is 'allowlist' and allowFrom is empty", () => {
|
|
145
|
+
expect(
|
|
146
|
+
isFeishuGroupAllowed({
|
|
147
|
+
groupPolicy: "allowlist",
|
|
148
|
+
allowFrom: [],
|
|
149
|
+
senderId: "oc_group_999",
|
|
150
|
+
}),
|
|
151
|
+
).toBe(false);
|
|
152
|
+
});
|
|
113
153
|
});
|
|
114
154
|
});
|
package/src/policy.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type {
|
|
|
2
2
|
AllowlistMatch,
|
|
3
3
|
ChannelGroupContext,
|
|
4
4
|
GroupToolPolicyConfig,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
5
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
6
|
+
import { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/feishu";
|
|
6
7
|
import { normalizeFeishuTarget } from "./targets.js";
|
|
7
8
|
import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
|
|
8
9
|
|
|
@@ -92,20 +93,18 @@ export function resolveFeishuGroupToolPolicy(
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
export function isFeishuGroupAllowed(params: {
|
|
95
|
-
groupPolicy: "open" | "allowlist" | "disabled";
|
|
96
|
+
groupPolicy: "open" | "allowlist" | "disabled" | "allowall";
|
|
96
97
|
allowFrom: Array<string | number>;
|
|
97
98
|
senderId: string;
|
|
98
99
|
senderIds?: Array<string | null | undefined>;
|
|
99
100
|
senderName?: string | null;
|
|
100
101
|
}): boolean {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
return resolveFeishuAllowlistMatch(params).allowed;
|
|
102
|
+
return evaluateSenderGroupAccessForPolicy({
|
|
103
|
+
groupPolicy: params.groupPolicy === "allowall" ? "open" : params.groupPolicy,
|
|
104
|
+
groupAllowFrom: params.allowFrom.map((entry) => String(entry)),
|
|
105
|
+
senderId: params.senderId,
|
|
106
|
+
isSenderAllowed: () => resolveFeishuAllowlistMatch(params).allowed,
|
|
107
|
+
}).allowed;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
export function resolveFeishuReplyPolicy(params: {
|
package/src/probe.test.ts
CHANGED
|
@@ -34,7 +34,7 @@ describe("probeFeishu", () => {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("returns error when appId is missing", async () => {
|
|
37
|
-
const result = await probeFeishu({ appSecret: "secret" } as never);
|
|
37
|
+
const result = await probeFeishu({ appSecret: "secret" } as never); // pragma: allowlist secret
|
|
38
38
|
expect(result).toEqual({ ok: false, error: "missing credentials (appId, appSecret)" });
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -49,7 +49,7 @@ describe("probeFeishu", () => {
|
|
|
49
49
|
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
const result = await probeFeishu({ appId: "cli_123", appSecret: "secret" });
|
|
52
|
+
const result = await probeFeishu({ appId: "cli_123", appSecret: "secret" }); // pragma: allowlist secret
|
|
53
53
|
expect(result).toEqual({
|
|
54
54
|
ok: true,
|
|
55
55
|
appId: "cli_123",
|
|
@@ -65,7 +65,7 @@ describe("probeFeishu", () => {
|
|
|
65
65
|
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
await probeFeishu({ appId: "cli_123", appSecret: "secret" });
|
|
68
|
+
await probeFeishu({ appId: "cli_123", appSecret: "secret" }); // pragma: allowlist secret
|
|
69
69
|
|
|
70
70
|
expect(requestFn).toHaveBeenCalledWith(
|
|
71
71
|
expect.objectContaining({
|
|
@@ -98,7 +98,7 @@ describe("probeFeishu", () => {
|
|
|
98
98
|
abortController.abort();
|
|
99
99
|
|
|
100
100
|
const result = await probeFeishu(
|
|
101
|
-
{ appId: "cli_123", appSecret: "secret" },
|
|
101
|
+
{ appId: "cli_123", appSecret: "secret" }, // pragma: allowlist secret
|
|
102
102
|
{ abortSignal: abortController.signal },
|
|
103
103
|
);
|
|
104
104
|
|
|
@@ -111,7 +111,7 @@ describe("probeFeishu", () => {
|
|
|
111
111
|
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
const creds = { appId: "cli_123", appSecret: "secret" };
|
|
114
|
+
const creds = { appId: "cli_123", appSecret: "secret" }; // pragma: allowlist secret
|
|
115
115
|
const first = await probeFeishu(creds);
|
|
116
116
|
const second = await probeFeishu(creds);
|
|
117
117
|
|
|
@@ -128,7 +128,7 @@ describe("probeFeishu", () => {
|
|
|
128
128
|
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
const creds = { appId: "cli_123", appSecret: "secret" };
|
|
131
|
+
const creds = { appId: "cli_123", appSecret: "secret" }; // pragma: allowlist secret
|
|
132
132
|
await probeFeishu(creds);
|
|
133
133
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
134
134
|
|
|
@@ -148,7 +148,7 @@ describe("probeFeishu", () => {
|
|
|
148
148
|
const requestFn = makeRequestFn({ code: 99, msg: "token expired" });
|
|
149
149
|
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
|
150
150
|
|
|
151
|
-
const creds = { appId: "cli_123", appSecret: "secret" };
|
|
151
|
+
const creds = { appId: "cli_123", appSecret: "secret" }; // pragma: allowlist secret
|
|
152
152
|
const first = await probeFeishu(creds);
|
|
153
153
|
const second = await probeFeishu(creds);
|
|
154
154
|
expect(first).toMatchObject({ ok: false, error: "API error: token expired" });
|
|
@@ -170,7 +170,7 @@ describe("probeFeishu", () => {
|
|
|
170
170
|
const requestFn = vi.fn().mockRejectedValue(new Error("network error"));
|
|
171
171
|
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
|
172
172
|
|
|
173
|
-
const creds = { appId: "cli_123", appSecret: "secret" };
|
|
173
|
+
const creds = { appId: "cli_123", appSecret: "secret" }; // pragma: allowlist secret
|
|
174
174
|
const first = await probeFeishu(creds);
|
|
175
175
|
const second = await probeFeishu(creds);
|
|
176
176
|
expect(first).toMatchObject({ ok: false, error: "network error" });
|
|
@@ -192,15 +192,15 @@ describe("probeFeishu", () => {
|
|
|
192
192
|
bot: { bot_name: "Bot1", open_id: "ou_1" },
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
await probeFeishu({ appId: "cli_aaa", appSecret: "s1" });
|
|
195
|
+
await probeFeishu({ appId: "cli_aaa", appSecret: "s1" }); // pragma: allowlist secret
|
|
196
196
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
197
197
|
|
|
198
198
|
// Different appId should trigger a new API call
|
|
199
|
-
await probeFeishu({ appId: "cli_bbb", appSecret: "s2" });
|
|
199
|
+
await probeFeishu({ appId: "cli_bbb", appSecret: "s2" }); // pragma: allowlist secret
|
|
200
200
|
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
201
201
|
|
|
202
202
|
// Same appId + appSecret as first call should return cached
|
|
203
|
-
await probeFeishu({ appId: "cli_aaa", appSecret: "s1" });
|
|
203
|
+
await probeFeishu({ appId: "cli_aaa", appSecret: "s1" }); // pragma: allowlist secret
|
|
204
204
|
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
205
205
|
});
|
|
206
206
|
|
|
@@ -211,12 +211,12 @@ describe("probeFeishu", () => {
|
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
// First account with appId + secret A
|
|
214
|
-
await probeFeishu({ appId: "cli_shared", appSecret: "secret_aaa" });
|
|
214
|
+
await probeFeishu({ appId: "cli_shared", appSecret: "secret_aaa" }); // pragma: allowlist secret
|
|
215
215
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
216
216
|
|
|
217
217
|
// Second account with same appId but different secret (e.g. after rotation)
|
|
218
218
|
// must NOT reuse the cached result
|
|
219
|
-
await probeFeishu({ appId: "cli_shared", appSecret: "secret_bbb" });
|
|
219
|
+
await probeFeishu({ appId: "cli_shared", appSecret: "secret_bbb" }); // pragma: allowlist secret
|
|
220
220
|
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
221
221
|
});
|
|
222
222
|
|
|
@@ -227,14 +227,14 @@ describe("probeFeishu", () => {
|
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
// Two accounts with same appId+appSecret but different accountIds are cached separately
|
|
230
|
-
await probeFeishu({ accountId: "acct-1", appId: "cli_123", appSecret: "secret" });
|
|
230
|
+
await probeFeishu({ accountId: "acct-1", appId: "cli_123", appSecret: "secret" }); // pragma: allowlist secret
|
|
231
231
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
232
232
|
|
|
233
|
-
await probeFeishu({ accountId: "acct-2", appId: "cli_123", appSecret: "secret" });
|
|
233
|
+
await probeFeishu({ accountId: "acct-2", appId: "cli_123", appSecret: "secret" }); // pragma: allowlist secret
|
|
234
234
|
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
235
235
|
|
|
236
236
|
// Same accountId should return cached
|
|
237
|
-
await probeFeishu({ accountId: "acct-1", appId: "cli_123", appSecret: "secret" });
|
|
237
|
+
await probeFeishu({ accountId: "acct-1", appId: "cli_123", appSecret: "secret" }); // pragma: allowlist secret
|
|
238
238
|
expect(requestFn).toHaveBeenCalledTimes(2);
|
|
239
239
|
});
|
|
240
240
|
|
|
@@ -244,7 +244,7 @@ describe("probeFeishu", () => {
|
|
|
244
244
|
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
-
const creds = { appId: "cli_123", appSecret: "secret" };
|
|
247
|
+
const creds = { appId: "cli_123", appSecret: "secret" }; // pragma: allowlist secret
|
|
248
248
|
await probeFeishu(creds);
|
|
249
249
|
expect(requestFn).toHaveBeenCalledTimes(1);
|
|
250
250
|
|
|
@@ -260,7 +260,7 @@ describe("probeFeishu", () => {
|
|
|
260
260
|
data: { bot: { bot_name: "DataBot", open_id: "ou_data" } },
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
-
const result = await probeFeishu({ appId: "cli_123", appSecret: "secret" });
|
|
263
|
+
const result = await probeFeishu({ appId: "cli_123", appSecret: "secret" }); // pragma: allowlist secret
|
|
264
264
|
expect(result).toEqual({
|
|
265
265
|
ok: true,
|
|
266
266
|
appId: "cli_123",
|
package/src/reactions.ts
CHANGED