@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.
- package/index.ts +2 -0
- package/package.json +2 -1
- package/skills/feishu-doc/SKILL.md +109 -3
- package/src/accounts.test.ts +90 -0
- package/src/accounts.ts +11 -2
- package/src/async.ts +62 -0
- package/src/bitable.ts +189 -215
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +55 -0
- package/src/bot.test.ts +863 -9
- package/src/bot.ts +414 -200
- package/src/card-action.ts +79 -0
- package/src/channel.ts +6 -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 +107 -0
- package/src/client.ts +13 -0
- package/src/config-schema.test.ts +82 -1
- package/src/config-schema.ts +54 -3
- package/src/doc-schema.ts +141 -0
- package/src/docx-batch-insert.ts +190 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +76 -0
- package/src/docx.test.ts +470 -0
- package/src/docx.ts +996 -72
- package/src/drive.ts +38 -33
- package/src/media.test.ts +123 -6
- package/src/media.ts +31 -10
- package/src/monitor.account.ts +286 -0
- package/src/monitor.reaction.test.ts +235 -0
- package/src/monitor.startup.test.ts +187 -0
- package/src/monitor.startup.ts +51 -0
- package/src/monitor.state.ts +76 -0
- package/src/monitor.transport.ts +163 -0
- package/src/monitor.ts +44 -346
- package/src/monitor.webhook-security.test.ts +27 -1
- package/src/outbound.test.ts +181 -0
- package/src/outbound.ts +94 -7
- package/src/perm.ts +37 -30
- package/src/policy.test.ts +56 -1
- package/src/policy.ts +5 -1
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +253 -0
- package/src/probe.ts +99 -7
- package/src/reply-dispatcher.test.ts +259 -0
- package/src/reply-dispatcher.ts +139 -45
- package/src/send.reply-fallback.test.ts +105 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +143 -18
- package/src/streaming-card.ts +131 -43
- package/src/targets.test.ts +26 -1
- package/src/targets.ts +11 -6
- 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/tools-config.test.ts +21 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +140 -10
- 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.
|
|
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
|
-
{
|
|
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
|
|
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
|
+
}
|