@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.
Files changed (70) hide show
  1. package/index.ts +2 -2
  2. package/package.json +1 -1
  3. package/src/accounts.test.ts +199 -13
  4. package/src/accounts.ts +45 -17
  5. package/src/bitable.ts +40 -28
  6. package/src/bot.checkBotMentioned.test.ts +8 -0
  7. package/src/bot.stripBotMention.test.ts +118 -22
  8. package/src/bot.test.ts +516 -9
  9. package/src/bot.ts +366 -109
  10. package/src/card-action.ts +1 -1
  11. package/src/channel.test.ts +1 -1
  12. package/src/channel.ts +52 -64
  13. package/src/chat.test.ts +2 -2
  14. package/src/chat.ts +1 -1
  15. package/src/client.test.ts +207 -4
  16. package/src/client.ts +70 -5
  17. package/src/config-schema.test.ts +14 -6
  18. package/src/config-schema.ts +5 -1
  19. package/src/dedup.ts +1 -1
  20. package/src/directory.test.ts +40 -0
  21. package/src/directory.ts +29 -50
  22. package/src/docx-batch-insert.test.ts +90 -0
  23. package/src/docx-batch-insert.ts +8 -11
  24. package/src/docx.account-selection.test.ts +3 -3
  25. package/src/docx.ts +1 -1
  26. package/src/drive.ts +13 -17
  27. package/src/dynamic-agent.ts +1 -1
  28. package/src/feishu-command-handler.ts +59 -0
  29. package/src/media.test.ts +60 -13
  30. package/src/media.ts +23 -9
  31. package/src/monitor.account.ts +19 -8
  32. package/src/monitor.reaction.test.ts +111 -105
  33. package/src/monitor.startup.test.ts +11 -10
  34. package/src/monitor.startup.ts +20 -7
  35. package/src/monitor.state.ts +4 -1
  36. package/src/monitor.test-mocks.ts +42 -9
  37. package/src/monitor.transport.ts +4 -1
  38. package/src/monitor.ts +4 -4
  39. package/src/monitor.webhook-security.test.ts +8 -23
  40. package/src/onboarding.status.test.ts +1 -1
  41. package/src/onboarding.test.ts +143 -0
  42. package/src/onboarding.ts +86 -71
  43. package/src/outbound.test.ts +178 -0
  44. package/src/outbound.ts +39 -6
  45. package/src/perm.ts +11 -15
  46. package/src/policy.test.ts +40 -0
  47. package/src/policy.ts +9 -10
  48. package/src/probe.test.ts +18 -18
  49. package/src/reactions.ts +1 -1
  50. package/src/reply-dispatcher.test.ts +175 -0
  51. package/src/reply-dispatcher.ts +69 -21
  52. package/src/runtime.ts +1 -1
  53. package/src/secret-input.ts +8 -14
  54. package/src/send-message.ts +71 -0
  55. package/src/send-target.test.ts +1 -1
  56. package/src/send-target.ts +1 -1
  57. package/src/send.reply-fallback.test.ts +74 -0
  58. package/src/send.test.ts +1 -1
  59. package/src/send.ts +88 -49
  60. package/src/streaming-card.test.ts +54 -0
  61. package/src/streaming-card.ts +96 -28
  62. package/src/targets.ts +5 -1
  63. package/src/tool-account-routing.test.ts +3 -3
  64. package/src/tool-account.ts +1 -1
  65. package/src/tool-factory-test-harness.ts +1 -1
  66. package/src/tool-result.test.ts +32 -0
  67. package/src/tool-result.ts +14 -0
  68. package/src/types.ts +2 -3
  69. package/src/typing.ts +1 -1
  70. package/src/wiki.ts +15 -19
@@ -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 ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
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
- // ============ Helpers ============
8
-
9
- function json(data: unknown) {
10
- return {
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 json(await listMembers(client, p.token, p.type));
153
+ return jsonToolResult(await listMembers(client, p.token, p.type));
158
154
  case "add":
159
- return json(
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 json(
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 json({ error: `Unknown action: ${(p as any).action}` });
164
+ return unknownToolActionResult((p as { action?: unknown }).action);
169
165
  }
170
166
  } catch (err) {
171
- return json({ error: err instanceof Error ? err.message : String(err) });
167
+ return toolExecutionErrorResult(err);
172
168
  }
173
169
  },
174
170
  };
@@ -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
- const { groupPolicy } = params;
102
- if (groupPolicy === "disabled") {
103
- return false;
104
- }
105
- if (groupPolicy === "open") {
106
- return true;
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
@@ -1,4 +1,4 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
2
2
  import { resolveFeishuAccount } from "./accounts.js";
3
3
  import { createFeishuClient } from "./client.js";
4
4