@openclaw/feishu 2026.2.25 → 2026.3.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.
Files changed (64) hide show
  1. package/index.ts +2 -0
  2. package/package.json +2 -1
  3. package/skills/feishu-doc/SKILL.md +109 -3
  4. package/src/accounts.test.ts +90 -0
  5. package/src/accounts.ts +11 -2
  6. package/src/async.ts +62 -0
  7. package/src/bitable.ts +189 -215
  8. package/src/bot.card-action.test.ts +63 -0
  9. package/src/bot.checkBotMentioned.test.ts +55 -0
  10. package/src/bot.test.ts +863 -9
  11. package/src/bot.ts +414 -200
  12. package/src/card-action.ts +79 -0
  13. package/src/channel.ts +6 -0
  14. package/src/chat-schema.ts +24 -0
  15. package/src/chat.test.ts +89 -0
  16. package/src/chat.ts +130 -0
  17. package/src/client.test.ts +107 -0
  18. package/src/client.ts +13 -0
  19. package/src/config-schema.test.ts +82 -1
  20. package/src/config-schema.ts +54 -3
  21. package/src/doc-schema.ts +141 -0
  22. package/src/docx-batch-insert.ts +190 -0
  23. package/src/docx-color-text.ts +149 -0
  24. package/src/docx-table-ops.ts +298 -0
  25. package/src/docx.account-selection.test.ts +76 -0
  26. package/src/docx.test.ts +470 -0
  27. package/src/docx.ts +996 -72
  28. package/src/drive.ts +38 -33
  29. package/src/media.test.ts +123 -6
  30. package/src/media.ts +31 -10
  31. package/src/monitor.account.ts +286 -0
  32. package/src/monitor.reaction.test.ts +235 -0
  33. package/src/monitor.startup.test.ts +187 -0
  34. package/src/monitor.startup.ts +51 -0
  35. package/src/monitor.state.ts +76 -0
  36. package/src/monitor.transport.ts +163 -0
  37. package/src/monitor.ts +44 -346
  38. package/src/monitor.webhook-security.test.ts +27 -1
  39. package/src/outbound.test.ts +181 -0
  40. package/src/outbound.ts +94 -7
  41. package/src/perm.ts +37 -30
  42. package/src/policy.test.ts +56 -1
  43. package/src/policy.ts +5 -1
  44. package/src/post.test.ts +105 -0
  45. package/src/post.ts +274 -0
  46. package/src/probe.test.ts +253 -0
  47. package/src/probe.ts +99 -7
  48. package/src/reply-dispatcher.test.ts +259 -0
  49. package/src/reply-dispatcher.ts +139 -45
  50. package/src/send.reply-fallback.test.ts +105 -0
  51. package/src/send.test.ts +168 -0
  52. package/src/send.ts +143 -18
  53. package/src/streaming-card.ts +131 -43
  54. package/src/targets.test.ts +26 -1
  55. package/src/targets.ts +11 -6
  56. package/src/tool-account-routing.test.ts +129 -0
  57. package/src/tool-account.ts +70 -0
  58. package/src/tool-factory-test-harness.ts +76 -0
  59. package/src/tools-config.test.ts +21 -0
  60. package/src/tools-config.ts +2 -1
  61. package/src/types.ts +1 -0
  62. package/src/typing.test.ts +144 -0
  63. package/src/typing.ts +140 -10
  64. package/src/wiki.ts +55 -50
package/index.ts CHANGED
@@ -2,6 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
3
  import { registerFeishuBitableTools } from "./src/bitable.js";
4
4
  import { feishuPlugin } from "./src/channel.js";
5
+ import { registerFeishuChatTools } from "./src/chat.js";
5
6
  import { registerFeishuDocTools } from "./src/docx.js";
6
7
  import { registerFeishuDriveTools } from "./src/drive.js";
7
8
  import { registerFeishuPermTools } from "./src/perm.js";
@@ -53,6 +54,7 @@ const plugin = {
53
54
  setFeishuRuntime(api.runtime);
54
55
  api.registerChannel({ plugin: feishuPlugin });
55
56
  registerFeishuDocTools(api);
57
+ registerFeishuChatTools(api);
56
58
  registerFeishuWikiTools(api);
57
59
  registerFeishuDriveTools(api);
58
60
  registerFeishuPermTools(api);
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/feishu",
3
- "version": "2026.2.25",
3
+ "version": "2026.3.1",
4
4
  "description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@larksuiteoapi/node-sdk": "^1.59.0",
8
8
  "@sinclair/typebox": "0.34.48",
9
+ "https-proxy-agent": "^7.0.6",
9
10
  "zod": "^4.3.6"
10
11
  },
11
12
  "openclaw": {
@@ -6,7 +6,7 @@ description: |
6
6
 
7
7
  # Feishu Document Tool
8
8
 
9
- Single tool `feishu_doc` with action parameter for all document operations.
9
+ Single tool `feishu_doc` with action parameter for all document operations, including table creation for Docx.
10
10
 
11
11
  ## Token Extraction
12
12
 
@@ -43,15 +43,22 @@ Appends markdown to end of document.
43
43
  ### Create Document
44
44
 
45
45
  ```json
46
- { "action": "create", "title": "New Document" }
46
+ { "action": "create", "title": "New Document", "owner_open_id": "ou_xxx" }
47
47
  ```
48
48
 
49
49
  With folder:
50
50
 
51
51
  ```json
52
- { "action": "create", "title": "New Document", "folder_token": "fldcnXXX" }
52
+ {
53
+ "action": "create",
54
+ "title": "New Document",
55
+ "folder_token": "fldcnXXX",
56
+ "owner_open_id": "ou_xxx"
57
+ }
53
58
  ```
54
59
 
60
+ **Important:** Always pass `owner_open_id` with the requesting user's `open_id` (from inbound metadata `sender_id`) so the user automatically gets `full_access` permission on the created document. Without this, only the bot app has access.
61
+
55
62
  ### List Blocks
56
63
 
57
64
  ```json
@@ -83,6 +90,105 @@ Returns full block data including tables, images. Use this to read structured co
83
90
  { "action": "delete_block", "doc_token": "ABC123def", "block_id": "doxcnXXX" }
84
91
  ```
85
92
 
93
+ ### Create Table (Docx Table Block)
94
+
95
+ ```json
96
+ {
97
+ "action": "create_table",
98
+ "doc_token": "ABC123def",
99
+ "row_size": 2,
100
+ "column_size": 2,
101
+ "column_width": [200, 200]
102
+ }
103
+ ```
104
+
105
+ Optional: `parent_block_id` to insert under a specific block.
106
+
107
+ ### Write Table Cells
108
+
109
+ ```json
110
+ {
111
+ "action": "write_table_cells",
112
+ "doc_token": "ABC123def",
113
+ "table_block_id": "doxcnTABLE",
114
+ "values": [
115
+ ["A1", "B1"],
116
+ ["A2", "B2"]
117
+ ]
118
+ }
119
+ ```
120
+
121
+ ### Create Table With Values (One-step)
122
+
123
+ ```json
124
+ {
125
+ "action": "create_table_with_values",
126
+ "doc_token": "ABC123def",
127
+ "row_size": 2,
128
+ "column_size": 2,
129
+ "column_width": [200, 200],
130
+ "values": [
131
+ ["A1", "B1"],
132
+ ["A2", "B2"]
133
+ ]
134
+ }
135
+ ```
136
+
137
+ Optional: `parent_block_id` to insert under a specific block.
138
+
139
+ ### Upload Image to Docx (from URL or local file)
140
+
141
+ ```json
142
+ {
143
+ "action": "upload_image",
144
+ "doc_token": "ABC123def",
145
+ "url": "https://example.com/image.png"
146
+ }
147
+ ```
148
+
149
+ Or local path with position control:
150
+
151
+ ```json
152
+ {
153
+ "action": "upload_image",
154
+ "doc_token": "ABC123def",
155
+ "file_path": "/tmp/image.png",
156
+ "parent_block_id": "doxcnParent",
157
+ "index": 5
158
+ }
159
+ ```
160
+
161
+ Optional `index` (0-based) inserts the image at a specific position among sibling blocks. Omit to append at end.
162
+
163
+ **Note:** Image display size is determined by the uploaded image's pixel dimensions. For small images (e.g. 480x270 GIFs), scale to 800px+ width before uploading to ensure proper display.
164
+
165
+ ### Upload File Attachment to Docx (from URL or local file)
166
+
167
+ ```json
168
+ {
169
+ "action": "upload_file",
170
+ "doc_token": "ABC123def",
171
+ "url": "https://example.com/report.pdf"
172
+ }
173
+ ```
174
+
175
+ Or local path:
176
+
177
+ ```json
178
+ {
179
+ "action": "upload_file",
180
+ "doc_token": "ABC123def",
181
+ "file_path": "/tmp/report.pdf",
182
+ "filename": "Q1-report.pdf"
183
+ }
184
+ ```
185
+
186
+ Rules:
187
+
188
+ - exactly one of `url` / `file_path`
189
+ - optional `filename` override
190
+ - optional `parent_block_id`
191
+
86
192
  ## Reading Workflow
87
193
 
88
194
  1. Start with `action: "read"` - get plain text + statistics
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveDefaultFeishuAccountId, resolveFeishuAccount } from "./accounts.js";
3
+
4
+ describe("resolveDefaultFeishuAccountId", () => {
5
+ it("prefers channels.feishu.defaultAccount when configured", () => {
6
+ const cfg = {
7
+ channels: {
8
+ feishu: {
9
+ defaultAccount: "router-d",
10
+ accounts: {
11
+ default: { appId: "cli_default", appSecret: "secret_default" },
12
+ "router-d": { appId: "cli_router", appSecret: "secret_router" },
13
+ },
14
+ },
15
+ },
16
+ };
17
+
18
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
19
+ });
20
+
21
+ it("normalizes configured defaultAccount before lookup", () => {
22
+ const cfg = {
23
+ channels: {
24
+ feishu: {
25
+ defaultAccount: "Router D",
26
+ accounts: {
27
+ "router-d": { appId: "cli_router", appSecret: "secret_router" },
28
+ },
29
+ },
30
+ },
31
+ };
32
+
33
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
34
+ });
35
+
36
+ it("falls back to literal default account id when preferred is missing", () => {
37
+ const cfg = {
38
+ channels: {
39
+ feishu: {
40
+ defaultAccount: "missing",
41
+ accounts: {
42
+ default: { appId: "cli_default", appSecret: "secret_default" },
43
+ zeta: { appId: "cli_zeta", appSecret: "secret_zeta" },
44
+ },
45
+ },
46
+ },
47
+ };
48
+
49
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("default");
50
+ });
51
+ });
52
+
53
+ describe("resolveFeishuAccount", () => {
54
+ it("uses configured default account when accountId is omitted", () => {
55
+ const cfg = {
56
+ channels: {
57
+ feishu: {
58
+ defaultAccount: "router-d",
59
+ accounts: {
60
+ default: { enabled: true },
61
+ "router-d": { appId: "cli_router", appSecret: "secret_router", enabled: true },
62
+ },
63
+ },
64
+ },
65
+ };
66
+
67
+ const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
68
+ expect(account.accountId).toBe("router-d");
69
+ expect(account.configured).toBe(true);
70
+ expect(account.appId).toBe("cli_router");
71
+ });
72
+
73
+ it("keeps explicit accountId selection", () => {
74
+ const cfg = {
75
+ channels: {
76
+ feishu: {
77
+ defaultAccount: "router-d",
78
+ accounts: {
79
+ default: { appId: "cli_default", appSecret: "secret_default" },
80
+ "router-d": { appId: "cli_router", appSecret: "secret_router" },
81
+ },
82
+ },
83
+ },
84
+ };
85
+
86
+ const account = resolveFeishuAccount({ cfg: cfg as never, accountId: "default" });
87
+ expect(account.accountId).toBe("default");
88
+ expect(account.appId).toBe("cli_default");
89
+ });
90
+ });
package/src/accounts.ts CHANGED
@@ -35,7 +35,12 @@ export function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
35
35
  * Resolve the default account ID.
36
36
  */
37
37
  export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
38
+ const preferredRaw = (cfg.channels?.feishu as FeishuConfig | undefined)?.defaultAccount?.trim();
39
+ const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : undefined;
38
40
  const ids = listFeishuAccountIds(cfg);
41
+ if (preferred && ids.includes(preferred)) {
42
+ return preferred;
43
+ }
39
44
  if (ids.includes(DEFAULT_ACCOUNT_ID)) {
40
45
  return DEFAULT_ACCOUNT_ID;
41
46
  }
@@ -64,7 +69,7 @@ function mergeFeishuAccountConfig(cfg: ClawdbotConfig, accountId: string): Feish
64
69
  const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
65
70
 
66
71
  // Extract base config (exclude accounts field to avoid recursion)
67
- const { accounts: _ignored, ...base } = feishuCfg ?? {};
72
+ const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...base } = feishuCfg ?? {};
68
73
 
69
74
  // Get account-specific overrides
70
75
  const account = resolveAccountConfig(cfg, accountId) ?? {};
@@ -104,7 +109,11 @@ export function resolveFeishuAccount(params: {
104
109
  cfg: ClawdbotConfig;
105
110
  accountId?: string | null;
106
111
  }): ResolvedFeishuAccount {
107
- const accountId = normalizeAccountId(params.accountId);
112
+ const hasExplicitAccountId =
113
+ typeof params.accountId === "string" && params.accountId.trim() !== "";
114
+ const accountId = hasExplicitAccountId
115
+ ? normalizeAccountId(params.accountId)
116
+ : resolveDefaultFeishuAccountId(params.cfg);
108
117
  const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
109
118
 
110
119
  // Base enabled state (top-level)
package/src/async.ts ADDED
@@ -0,0 +1,62 @@
1
+ const RACE_TIMEOUT = Symbol("race-timeout");
2
+ const RACE_ABORT = Symbol("race-abort");
3
+
4
+ export type RaceWithTimeoutAndAbortResult<T> =
5
+ | { status: "resolved"; value: T }
6
+ | { status: "timeout" }
7
+ | { status: "aborted" };
8
+
9
+ export async function raceWithTimeoutAndAbort<T>(
10
+ promise: Promise<T>,
11
+ options: {
12
+ timeoutMs?: number;
13
+ abortSignal?: AbortSignal;
14
+ } = {},
15
+ ): Promise<RaceWithTimeoutAndAbortResult<T>> {
16
+ if (options.abortSignal?.aborted) {
17
+ return { status: "aborted" };
18
+ }
19
+
20
+ if (options.timeoutMs === undefined && !options.abortSignal) {
21
+ return { status: "resolved", value: await promise };
22
+ }
23
+
24
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
25
+ let abortHandler: (() => void) | undefined;
26
+ const contenders: Array<Promise<T | typeof RACE_TIMEOUT | typeof RACE_ABORT>> = [promise];
27
+
28
+ if (options.timeoutMs !== undefined) {
29
+ contenders.push(
30
+ new Promise((resolve) => {
31
+ timeoutHandle = setTimeout(() => resolve(RACE_TIMEOUT), options.timeoutMs);
32
+ }),
33
+ );
34
+ }
35
+
36
+ if (options.abortSignal) {
37
+ contenders.push(
38
+ new Promise((resolve) => {
39
+ abortHandler = () => resolve(RACE_ABORT);
40
+ options.abortSignal?.addEventListener("abort", abortHandler, { once: true });
41
+ }),
42
+ );
43
+ }
44
+
45
+ try {
46
+ const result = await Promise.race(contenders);
47
+ if (result === RACE_TIMEOUT) {
48
+ return { status: "timeout" };
49
+ }
50
+ if (result === RACE_ABORT) {
51
+ return { status: "aborted" };
52
+ }
53
+ return { status: "resolved", value: result };
54
+ } finally {
55
+ if (timeoutHandle) {
56
+ clearTimeout(timeoutHandle);
57
+ }
58
+ if (abortHandler) {
59
+ options.abortSignal?.removeEventListener("abort", abortHandler);
60
+ }
61
+ }
62
+ }