@openclaw/feishu 2026.3.1 → 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 (76) hide show
  1. package/index.ts +2 -2
  2. package/package.json +1 -1
  3. package/src/accounts.test.ts +268 -11
  4. package/src/accounts.ts +101 -14
  5. package/src/bitable.ts +40 -28
  6. package/src/bot.checkBotMentioned.test.ts +9 -1
  7. package/src/bot.stripBotMention.test.ts +118 -22
  8. package/src/bot.test.ts +945 -77
  9. package/src/bot.ts +492 -165
  10. package/src/card-action.ts +1 -1
  11. package/src/channel.test.ts +1 -1
  12. package/src/channel.ts +72 -68
  13. package/src/chat.test.ts +2 -2
  14. package/src/chat.ts +1 -1
  15. package/src/client.test.ts +221 -4
  16. package/src/client.ts +70 -5
  17. package/src/config-schema.test.ts +33 -6
  18. package/src/config-schema.ts +18 -10
  19. package/src/dedup.ts +47 -1
  20. package/src/directory.test.ts +40 -0
  21. package/src/directory.ts +29 -50
  22. package/src/doc-schema.ts +16 -22
  23. package/src/docx-batch-insert.test.ts +90 -0
  24. package/src/docx-batch-insert.ts +8 -11
  25. package/src/docx.account-selection.test.ts +10 -16
  26. package/src/docx.test.ts +41 -189
  27. package/src/docx.ts +1 -1
  28. package/src/drive.ts +13 -17
  29. package/src/dynamic-agent.ts +1 -1
  30. package/src/feishu-command-handler.ts +59 -0
  31. package/src/media.test.ts +164 -14
  32. package/src/media.ts +44 -10
  33. package/src/mention.ts +1 -1
  34. package/src/monitor.account.ts +284 -25
  35. package/src/monitor.reaction.test.ts +395 -46
  36. package/src/monitor.startup.test.ts +25 -8
  37. package/src/monitor.startup.ts +20 -7
  38. package/src/monitor.state.defaults.test.ts +46 -0
  39. package/src/monitor.state.ts +88 -9
  40. package/src/monitor.test-mocks.ts +45 -0
  41. package/src/monitor.transport.ts +4 -1
  42. package/src/monitor.ts +4 -4
  43. package/src/monitor.webhook-security.test.ts +13 -11
  44. package/src/onboarding.status.test.ts +25 -0
  45. package/src/onboarding.test.ts +143 -0
  46. package/src/onboarding.ts +213 -106
  47. package/src/outbound.test.ts +178 -0
  48. package/src/outbound.ts +39 -6
  49. package/src/perm.ts +11 -15
  50. package/src/policy.test.ts +40 -0
  51. package/src/policy.ts +9 -10
  52. package/src/probe.test.ts +54 -36
  53. package/src/probe.ts +57 -37
  54. package/src/reactions.ts +1 -1
  55. package/src/reply-dispatcher.test.ts +216 -0
  56. package/src/reply-dispatcher.ts +89 -22
  57. package/src/runtime.ts +1 -1
  58. package/src/secret-input.ts +13 -0
  59. package/src/send-message.ts +71 -0
  60. package/src/send-target.test.ts +74 -0
  61. package/src/send-target.ts +7 -3
  62. package/src/send.reply-fallback.test.ts +74 -0
  63. package/src/send.test.ts +1 -1
  64. package/src/send.ts +88 -49
  65. package/src/streaming-card.test.ts +54 -0
  66. package/src/streaming-card.ts +96 -28
  67. package/src/targets.test.ts +29 -0
  68. package/src/targets.ts +25 -1
  69. package/src/tool-account-routing.test.ts +3 -3
  70. package/src/tool-account.ts +1 -1
  71. package/src/tool-factory-test-harness.ts +1 -1
  72. package/src/tool-result.test.ts +32 -0
  73. package/src/tool-result.ts +14 -0
  74. package/src/types.ts +11 -4
  75. package/src/typing.ts +1 -1
  76. package/src/wiki.ts +15 -19
package/src/docx.test.ts CHANGED
@@ -114,6 +114,29 @@ describe("feishu_doc image fetch hardening", () => {
114
114
  scopeListMock.mockResolvedValue({ code: 0, data: { scopes: [] } });
115
115
  });
116
116
 
117
+ function resolveFeishuDocTool(context: Record<string, unknown> = {}) {
118
+ const registerTool = vi.fn();
119
+ registerFeishuDocTools({
120
+ config: {
121
+ channels: {
122
+ feishu: {
123
+ appId: "app_id",
124
+ appSecret: "app_secret",
125
+ },
126
+ },
127
+ } as any,
128
+ logger: { debug: vi.fn(), info: vi.fn() } as any,
129
+ registerTool,
130
+ } as any);
131
+
132
+ const tool = registerTool.mock.calls
133
+ .map((call) => call[0])
134
+ .map((candidate) => (typeof candidate === "function" ? candidate(context) : candidate))
135
+ .find((candidate) => candidate.name === "feishu_doc");
136
+ expect(tool).toBeDefined();
137
+ return tool as { execute: (callId: string, params: Record<string, unknown>) => Promise<any> };
138
+ }
139
+
117
140
  it("inserts blocks sequentially to preserve document order", async () => {
118
141
  const blocks = [
119
142
  { block_type: 3, block_id: "h1" },
@@ -135,22 +158,7 @@ describe("feishu_doc image fetch hardening", () => {
135
158
  data: { children: [{ block_type: 3, block_id: "h1" }] },
136
159
  });
137
160
 
138
- const registerTool = vi.fn();
139
- registerFeishuDocTools({
140
- config: {
141
- channels: {
142
- feishu: { appId: "app_id", appSecret: "app_secret" },
143
- },
144
- } as any,
145
- logger: { debug: vi.fn(), info: vi.fn() } as any,
146
- registerTool,
147
- } as any);
148
-
149
- const feishuDocTool = registerTool.mock.calls
150
- .map((call) => call[0])
151
- .map((tool) => (typeof tool === "function" ? tool({}) : tool))
152
- .find((tool) => tool.name === "feishu_doc");
153
- expect(feishuDocTool).toBeDefined();
161
+ const feishuDocTool = resolveFeishuDocTool();
154
162
 
155
163
  const result = await feishuDocTool.execute("tool-call", {
156
164
  action: "append",
@@ -194,22 +202,7 @@ describe("feishu_doc image fetch hardening", () => {
194
202
  },
195
203
  }));
196
204
 
197
- const registerTool = vi.fn();
198
- registerFeishuDocTools({
199
- config: {
200
- channels: {
201
- feishu: { appId: "app_id", appSecret: "app_secret" },
202
- },
203
- } as any,
204
- logger: { debug: vi.fn(), info: vi.fn() } as any,
205
- registerTool,
206
- } as any);
207
-
208
- const feishuDocTool = registerTool.mock.calls
209
- .map((call) => call[0])
210
- .map((tool) => (typeof tool === "function" ? tool({}) : tool))
211
- .find((tool) => tool.name === "feishu_doc");
212
- expect(feishuDocTool).toBeDefined();
205
+ const feishuDocTool = resolveFeishuDocTool();
213
206
 
214
207
  const longMarkdown = Array.from(
215
208
  { length: 120 },
@@ -254,22 +247,7 @@ describe("feishu_doc image fetch hardening", () => {
254
247
  data: { children: data.children },
255
248
  }));
256
249
 
257
- const registerTool = vi.fn();
258
- registerFeishuDocTools({
259
- config: {
260
- channels: {
261
- feishu: { appId: "app_id", appSecret: "app_secret" },
262
- },
263
- } as any,
264
- logger: { debug: vi.fn(), info: vi.fn() } as any,
265
- registerTool,
266
- } as any);
267
-
268
- const feishuDocTool = registerTool.mock.calls
269
- .map((call) => call[0])
270
- .map((tool) => (typeof tool === "function" ? tool({}) : tool))
271
- .find((tool) => tool.name === "feishu_doc");
272
- expect(feishuDocTool).toBeDefined();
250
+ const feishuDocTool = resolveFeishuDocTool();
273
251
 
274
252
  const fencedMarkdown = [
275
253
  "## Section",
@@ -306,25 +284,7 @@ describe("feishu_doc image fetch hardening", () => {
306
284
  new Error("Blocked: resolves to private/internal IP address"),
307
285
  );
308
286
 
309
- const registerTool = vi.fn();
310
- registerFeishuDocTools({
311
- config: {
312
- channels: {
313
- feishu: {
314
- appId: "app_id",
315
- appSecret: "app_secret",
316
- },
317
- },
318
- } as any,
319
- logger: { debug: vi.fn(), info: vi.fn() } as any,
320
- registerTool,
321
- } as any);
322
-
323
- const feishuDocTool = registerTool.mock.calls
324
- .map((call) => call[0])
325
- .map((tool) => (typeof tool === "function" ? tool({}) : tool))
326
- .find((tool) => tool.name === "feishu_doc");
327
- expect(feishuDocTool).toBeDefined();
287
+ const feishuDocTool = resolveFeishuDocTool();
328
288
 
329
289
  const result = await feishuDocTool.execute("tool-call", {
330
290
  action: "write",
@@ -341,29 +301,10 @@ describe("feishu_doc image fetch hardening", () => {
341
301
  });
342
302
 
343
303
  it("create grants permission only to trusted Feishu requester", async () => {
344
- const registerTool = vi.fn();
345
- registerFeishuDocTools({
346
- config: {
347
- channels: {
348
- feishu: {
349
- appId: "app_id",
350
- appSecret: "app_secret",
351
- },
352
- },
353
- } as any,
354
- logger: { debug: vi.fn(), info: vi.fn() } as any,
355
- registerTool,
356
- } as any);
357
-
358
- const feishuDocTool = registerTool.mock.calls
359
- .map((call) => call[0])
360
- .map((tool) =>
361
- typeof tool === "function"
362
- ? tool({ messageChannel: "feishu", requesterSenderId: "ou_123" })
363
- : tool,
364
- )
365
- .find((tool) => tool.name === "feishu_doc");
366
- expect(feishuDocTool).toBeDefined();
304
+ const feishuDocTool = resolveFeishuDocTool({
305
+ messageChannel: "feishu",
306
+ requesterSenderId: "ou_123",
307
+ });
367
308
 
368
309
  const result = await feishuDocTool.execute("tool-call", {
369
310
  action: "create",
@@ -386,25 +327,9 @@ describe("feishu_doc image fetch hardening", () => {
386
327
  });
387
328
 
388
329
  it("create skips requester grant when trusted requester identity is unavailable", async () => {
389
- const registerTool = vi.fn();
390
- registerFeishuDocTools({
391
- config: {
392
- channels: {
393
- feishu: {
394
- appId: "app_id",
395
- appSecret: "app_secret",
396
- },
397
- },
398
- } as any,
399
- logger: { debug: vi.fn(), info: vi.fn() } as any,
400
- registerTool,
401
- } as any);
402
-
403
- const feishuDocTool = registerTool.mock.calls
404
- .map((call) => call[0])
405
- .map((tool) => (typeof tool === "function" ? tool({ messageChannel: "feishu" }) : tool))
406
- .find((tool) => tool.name === "feishu_doc");
407
- expect(feishuDocTool).toBeDefined();
330
+ const feishuDocTool = resolveFeishuDocTool({
331
+ messageChannel: "feishu",
332
+ });
408
333
 
409
334
  const result = await feishuDocTool.execute("tool-call", {
410
335
  action: "create",
@@ -417,29 +342,10 @@ describe("feishu_doc image fetch hardening", () => {
417
342
  });
418
343
 
419
344
  it("create never grants permissions when grant_to_requester is false", async () => {
420
- const registerTool = vi.fn();
421
- registerFeishuDocTools({
422
- config: {
423
- channels: {
424
- feishu: {
425
- appId: "app_id",
426
- appSecret: "app_secret",
427
- },
428
- },
429
- } as any,
430
- logger: { debug: vi.fn(), info: vi.fn() } as any,
431
- registerTool,
432
- } as any);
433
-
434
- const feishuDocTool = registerTool.mock.calls
435
- .map((call) => call[0])
436
- .map((tool) =>
437
- typeof tool === "function"
438
- ? tool({ messageChannel: "feishu", requesterSenderId: "ou_123" })
439
- : tool,
440
- )
441
- .find((tool) => tool.name === "feishu_doc");
442
- expect(feishuDocTool).toBeDefined();
345
+ const feishuDocTool = resolveFeishuDocTool({
346
+ messageChannel: "feishu",
347
+ requesterSenderId: "ou_123",
348
+ });
443
349
 
444
350
  const result = await feishuDocTool.execute("tool-call", {
445
351
  action: "create",
@@ -457,25 +363,7 @@ describe("feishu_doc image fetch hardening", () => {
457
363
  data: { document: { title: "Created Doc" } },
458
364
  });
459
365
 
460
- const registerTool = vi.fn();
461
- registerFeishuDocTools({
462
- config: {
463
- channels: {
464
- feishu: {
465
- appId: "app_id",
466
- appSecret: "app_secret",
467
- },
468
- },
469
- } as any,
470
- logger: { debug: vi.fn(), info: vi.fn() } as any,
471
- registerTool,
472
- } as any);
473
-
474
- const feishuDocTool = registerTool.mock.calls
475
- .map((call) => call[0])
476
- .map((tool) => (typeof tool === "function" ? tool({}) : tool))
477
- .find((tool) => tool.name === "feishu_doc");
478
- expect(feishuDocTool).toBeDefined();
366
+ const feishuDocTool = resolveFeishuDocTool();
479
367
 
480
368
  const result = await feishuDocTool.execute("tool-call", {
481
369
  action: "create",
@@ -496,25 +384,7 @@ describe("feishu_doc image fetch hardening", () => {
496
384
  const localPath = join(tmpdir(), `feishu-docx-upload-${Date.now()}.txt`);
497
385
  await fs.writeFile(localPath, "hello from local file", "utf8");
498
386
 
499
- const registerTool = vi.fn();
500
- registerFeishuDocTools({
501
- config: {
502
- channels: {
503
- feishu: {
504
- appId: "app_id",
505
- appSecret: "app_secret",
506
- },
507
- },
508
- } as any,
509
- logger: { debug: vi.fn(), info: vi.fn() } as any,
510
- registerTool,
511
- } as any);
512
-
513
- const feishuDocTool = registerTool.mock.calls
514
- .map((call) => call[0])
515
- .map((tool) => (typeof tool === "function" ? tool({}) : tool))
516
- .find((tool) => tool.name === "feishu_doc");
517
- expect(feishuDocTool).toBeDefined();
387
+ const feishuDocTool = resolveFeishuDocTool();
518
388
 
519
389
  const result = await feishuDocTool.execute("tool-call", {
520
390
  action: "upload_file",
@@ -557,25 +427,7 @@ describe("feishu_doc image fetch hardening", () => {
557
427
  await fs.writeFile(localPath, "hello from local file", "utf8");
558
428
 
559
429
  try {
560
- const registerTool = vi.fn();
561
- registerFeishuDocTools({
562
- config: {
563
- channels: {
564
- feishu: {
565
- appId: "app_id",
566
- appSecret: "app_secret",
567
- },
568
- },
569
- } as any,
570
- logger: { debug: vi.fn(), info: vi.fn() } as any,
571
- registerTool,
572
- } as any);
573
-
574
- const feishuDocTool = registerTool.mock.calls
575
- .map((call) => call[0])
576
- .map((tool) => (typeof tool === "function" ? tool({}) : tool))
577
- .find((tool) => tool.name === "feishu_doc");
578
- expect(feishuDocTool).toBeDefined();
430
+ const feishuDocTool = resolveFeishuDocTool();
579
431
 
580
432
  const result = await feishuDocTool.execute("tool-call", {
581
433
  action: "upload_file",
package/src/docx.ts CHANGED
@@ -4,7 +4,7 @@ import { isAbsolute } from "node:path";
4
4
  import { basename } from "node:path";
5
5
  import type * as Lark from "@larksuiteoapi/node-sdk";
6
6
  import { Type } from "@sinclair/typebox";
7
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
7
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
8
8
  import { listEnabledFeishuAccounts } from "./accounts.js";
9
9
  import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
10
10
  import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js";
package/src/drive.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 { FeishuDriveSchema, type FeishuDriveParams } from "./drive-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
  // ============ Actions ============
17
13
 
@@ -206,21 +202,21 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
206
202
  });
207
203
  switch (p.action) {
208
204
  case "list":
209
- return json(await listFolder(client, p.folder_token));
205
+ return jsonToolResult(await listFolder(client, p.folder_token));
210
206
  case "info":
211
- return json(await getFileInfo(client, p.file_token));
207
+ return jsonToolResult(await getFileInfo(client, p.file_token));
212
208
  case "create_folder":
213
- return json(await createFolder(client, p.name, p.folder_token));
209
+ return jsonToolResult(await createFolder(client, p.name, p.folder_token));
214
210
  case "move":
215
- return json(await moveFile(client, p.file_token, p.type, p.folder_token));
211
+ return jsonToolResult(await moveFile(client, p.file_token, p.type, p.folder_token));
216
212
  case "delete":
217
- return json(await deleteFile(client, p.file_token, p.type));
213
+ return jsonToolResult(await deleteFile(client, p.file_token, p.type));
218
214
  default:
219
215
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
220
- return json({ error: `Unknown action: ${(p as any).action}` });
216
+ return unknownToolActionResult((p as { action?: unknown }).action);
221
217
  }
222
218
  } catch (err) {
223
- return json({ error: err instanceof Error ? err.message : String(err) });
219
+ return toolExecutionErrorResult(err);
224
220
  }
225
221
  },
226
222
  };
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
4
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/feishu";
5
5
  import type { DynamicAgentCreationConfig } from "./types.js";
6
6
 
7
7
  export type MaybeCreateDynamicAgentResult = {
@@ -0,0 +1,59 @@
1
+ const DEFAULT_RESET_TRIGGERS = ["/new", "/reset"] as const;
2
+
3
+ type FeishuBeforeResetContext = {
4
+ cfg: Record<string, unknown>;
5
+ sessionEntry: Record<string, unknown>;
6
+ previousSessionEntry?: Record<string, unknown>;
7
+ commandSource: string;
8
+ timestamp: number;
9
+ };
10
+
11
+ type FeishuBeforeResetEvent = {
12
+ type: "command";
13
+ action: "new" | "reset";
14
+ context: FeishuBeforeResetContext;
15
+ };
16
+
17
+ type FeishuBeforeResetRunner = {
18
+ runBeforeReset: (
19
+ event: FeishuBeforeResetEvent,
20
+ ctx: { agentId: string; sessionKey: string },
21
+ ) => Promise<void>;
22
+ };
23
+
24
+ /**
25
+ * Handle Feishu command messages and trigger reset hooks.
26
+ */
27
+ export async function handleFeishuCommand(
28
+ messageText: string,
29
+ sessionKey: string,
30
+ hookRunner: FeishuBeforeResetRunner,
31
+ context: FeishuBeforeResetContext,
32
+ ): Promise<boolean> {
33
+ const trimmed = messageText.trim().toLowerCase();
34
+ const isResetCommand = DEFAULT_RESET_TRIGGERS.some(
35
+ (trigger) => trimmed === trigger || trimmed.startsWith(`${trigger} `),
36
+ );
37
+ if (!isResetCommand) {
38
+ return false;
39
+ }
40
+
41
+ const command = trimmed.split(" ")[0];
42
+ const action: "new" | "reset" = command === "/new" ? "new" : "reset";
43
+ await hookRunner.runBeforeReset(
44
+ {
45
+ type: "command",
46
+ action,
47
+ context: {
48
+ ...context,
49
+ commandSource: "feishu",
50
+ },
51
+ },
52
+ {
53
+ agentId: "main",
54
+ sessionKey,
55
+ },
56
+ );
57
+
58
+ return true;
59
+ }