@m1heng-clawd/feishu 0.1.11 → 0.1.12

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 (46) hide show
  1. package/README.md +123 -4
  2. package/index.ts +6 -6
  3. package/package.json +13 -3
  4. package/skills/feishu-doc/SKILL.md +40 -2
  5. package/skills/feishu-task/SKILL.md +210 -0
  6. package/src/bitable-tools/index.ts +1 -0
  7. package/src/bot.ts +181 -70
  8. package/src/channel.ts +2 -0
  9. package/src/config-schema.ts +4 -0
  10. package/src/doc-tools/actions.ts +341 -0
  11. package/src/doc-tools/common.ts +33 -0
  12. package/src/doc-tools/index.ts +2 -0
  13. package/src/doc-tools/register.ts +90 -0
  14. package/src/{doc-schema.ts → doc-tools/schemas.ts} +31 -1
  15. package/src/drive-tools/actions.ts +182 -0
  16. package/src/drive-tools/common.ts +18 -0
  17. package/src/drive-tools/index.ts +2 -0
  18. package/src/drive-tools/register.ts +71 -0
  19. package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
  20. package/src/media.ts +5 -3
  21. package/src/perm-tools/actions.ts +111 -0
  22. package/src/perm-tools/common.ts +18 -0
  23. package/src/perm-tools/index.ts +2 -0
  24. package/src/perm-tools/register.ts +65 -0
  25. package/src/policy.ts +13 -0
  26. package/src/reply-dispatcher.ts +6 -4
  27. package/src/send.ts +33 -2
  28. package/src/task-tools/actions.ts +466 -13
  29. package/src/task-tools/constants.ts +13 -0
  30. package/src/task-tools/index.ts +1 -0
  31. package/src/task-tools/register.ts +175 -13
  32. package/src/task-tools/schemas.ts +433 -4
  33. package/src/text/markdown-links.ts +104 -0
  34. package/src/types.ts +1 -0
  35. package/src/wiki-tools/actions.ts +166 -0
  36. package/src/wiki-tools/common.ts +18 -0
  37. package/src/wiki-tools/index.ts +2 -0
  38. package/src/wiki-tools/register.ts +66 -0
  39. package/src/bitable.ts +0 -1
  40. package/src/docx.ts +0 -276
  41. package/src/drive.ts +0 -237
  42. package/src/perm.ts +0 -165
  43. package/src/task.ts +0 -1
  44. package/src/wiki.ts +0 -223
  45. /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
  46. /package/src/{wiki-schema.ts → wiki-tools/schemas.ts} +0 -0
@@ -0,0 +1,166 @@
1
+ import { runWikiApiCall, type WikiClient } from "./common.js";
2
+ import type { FeishuWikiParams } from "./schemas.js";
3
+
4
+ type ObjType = "doc" | "sheet" | "mindnote" | "bitable" | "file" | "docx" | "slides";
5
+
6
+ const WIKI_ACCESS_HINT =
7
+ "To grant wiki access: Open wiki space -> Settings -> Members -> Add the bot. " +
8
+ "See: https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa#a40ad4ca";
9
+
10
+ async function listSpaces(client: WikiClient) {
11
+ const res = await runWikiApiCall("wiki.space.list", () => client.wiki.space.list({}));
12
+ const spaces =
13
+ res.data?.items?.map((s) => ({
14
+ space_id: s.space_id,
15
+ name: s.name,
16
+ description: s.description,
17
+ visibility: s.visibility,
18
+ })) ?? [];
19
+
20
+ return {
21
+ spaces,
22
+ ...(spaces.length === 0 && { hint: WIKI_ACCESS_HINT }),
23
+ };
24
+ }
25
+
26
+ async function listNodes(client: WikiClient, spaceId: string, parentNodeToken?: string) {
27
+ const res = await runWikiApiCall("wiki.spaceNode.list", () =>
28
+ client.wiki.spaceNode.list({
29
+ path: { space_id: spaceId },
30
+ params: { parent_node_token: parentNodeToken },
31
+ }),
32
+ );
33
+
34
+ return {
35
+ nodes:
36
+ res.data?.items?.map((n) => ({
37
+ node_token: n.node_token,
38
+ obj_token: n.obj_token,
39
+ obj_type: n.obj_type,
40
+ title: n.title,
41
+ has_child: n.has_child,
42
+ })) ?? [],
43
+ };
44
+ }
45
+
46
+ async function getNode(client: WikiClient, token: string) {
47
+ const res = await runWikiApiCall("wiki.space.getNode", () =>
48
+ client.wiki.space.getNode({
49
+ params: { token },
50
+ }),
51
+ );
52
+
53
+ const node = res.data?.node;
54
+ return {
55
+ node_token: node?.node_token,
56
+ space_id: node?.space_id,
57
+ obj_token: node?.obj_token,
58
+ obj_type: node?.obj_type,
59
+ title: node?.title,
60
+ parent_node_token: node?.parent_node_token,
61
+ has_child: node?.has_child,
62
+ creator: node?.creator,
63
+ create_time: node?.node_create_time,
64
+ };
65
+ }
66
+
67
+ async function createNode(
68
+ client: WikiClient,
69
+ spaceId: string,
70
+ title: string,
71
+ objType?: string,
72
+ parentNodeToken?: string,
73
+ ) {
74
+ const res = await runWikiApiCall("wiki.spaceNode.create", () =>
75
+ client.wiki.spaceNode.create({
76
+ path: { space_id: spaceId },
77
+ data: {
78
+ obj_type: (objType as ObjType) || "docx",
79
+ node_type: "origin" as const,
80
+ title,
81
+ parent_node_token: parentNodeToken,
82
+ },
83
+ }),
84
+ );
85
+
86
+ const node = res.data?.node;
87
+ return {
88
+ node_token: node?.node_token,
89
+ obj_token: node?.obj_token,
90
+ obj_type: node?.obj_type,
91
+ title: node?.title,
92
+ };
93
+ }
94
+
95
+ async function moveNode(
96
+ client: WikiClient,
97
+ spaceId: string,
98
+ nodeToken: string,
99
+ targetSpaceId?: string,
100
+ targetParentToken?: string,
101
+ ) {
102
+ const res = await runWikiApiCall("wiki.spaceNode.move", () =>
103
+ client.wiki.spaceNode.move({
104
+ path: { space_id: spaceId, node_token: nodeToken },
105
+ data: {
106
+ target_space_id: targetSpaceId || spaceId,
107
+ target_parent_token: targetParentToken,
108
+ },
109
+ }),
110
+ );
111
+
112
+ return {
113
+ success: true,
114
+ node_token: res.data?.node?.node_token,
115
+ };
116
+ }
117
+
118
+ async function renameNode(
119
+ client: WikiClient,
120
+ spaceId: string,
121
+ nodeToken: string,
122
+ title: string,
123
+ ) {
124
+ await runWikiApiCall("wiki.spaceNode.updateTitle", () =>
125
+ client.wiki.spaceNode.updateTitle({
126
+ path: { space_id: spaceId, node_token: nodeToken },
127
+ data: { title },
128
+ }),
129
+ );
130
+
131
+ return {
132
+ success: true,
133
+ node_token: nodeToken,
134
+ title,
135
+ };
136
+ }
137
+
138
+ export async function runWikiAction(client: WikiClient, params: FeishuWikiParams) {
139
+ switch (params.action) {
140
+ case "spaces":
141
+ return listSpaces(client);
142
+ case "nodes":
143
+ return listNodes(client, params.space_id, params.parent_node_token);
144
+ case "get":
145
+ return getNode(client, params.token);
146
+ case "search":
147
+ return {
148
+ error:
149
+ "Search is not available. Use feishu_wiki with action: 'nodes' to browse or action: 'get' to lookup by token.",
150
+ };
151
+ case "create":
152
+ return createNode(client, params.space_id, params.title, params.obj_type, params.parent_node_token);
153
+ case "move":
154
+ return moveNode(
155
+ client,
156
+ params.space_id,
157
+ params.node_token,
158
+ params.target_space_id,
159
+ params.target_parent_token,
160
+ );
161
+ case "rename":
162
+ return renameNode(client, params.space_id, params.node_token, params.title);
163
+ default:
164
+ return { error: `Unknown action: ${(params as any).action}` };
165
+ }
166
+ }
@@ -0,0 +1,18 @@
1
+ import { createFeishuClient } from "../client.js";
2
+ import {
3
+ errorResult,
4
+ json,
5
+ runFeishuApiCall,
6
+ type FeishuApiResponse,
7
+ } from "../tools-common/feishu-api.js";
8
+
9
+ export type WikiClient = ReturnType<typeof createFeishuClient>;
10
+
11
+ export { json, errorResult };
12
+
13
+ export async function runWikiApiCall<T extends FeishuApiResponse>(
14
+ context: string,
15
+ fn: () => Promise<T>,
16
+ ): Promise<T> {
17
+ return runFeishuApiCall(context, fn);
18
+ }
@@ -0,0 +1,2 @@
1
+ export { registerFeishuWikiTools } from "./register.js";
2
+ export { FeishuWikiSchema, type FeishuWikiParams } from "./schemas.js";
@@ -0,0 +1,66 @@
1
+ import type { TSchema } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
+ import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
4
+ import { runWikiAction } from "./actions.js";
5
+ import { errorResult, json, type WikiClient } from "./common.js";
6
+ import { FeishuWikiSchema, type FeishuWikiParams } from "./schemas.js";
7
+
8
+ type WikiToolSpec<P> = {
9
+ name: string;
10
+ label: string;
11
+ description: string;
12
+ parameters: TSchema;
13
+ run: (client: WikiClient, params: P) => Promise<unknown>;
14
+ };
15
+
16
+ function registerWikiTool<P>(api: OpenClawPluginApi, spec: WikiToolSpec<P>) {
17
+ api.registerTool(
18
+ {
19
+ name: spec.name,
20
+ label: spec.label,
21
+ description: spec.description,
22
+ parameters: spec.parameters,
23
+ async execute(_toolCallId, params) {
24
+ try {
25
+ return await withFeishuToolClient({
26
+ api,
27
+ toolName: spec.name,
28
+ requiredTool: "wiki",
29
+ run: async ({ client }) => json(await spec.run(client as WikiClient, params as P)),
30
+ });
31
+ } catch (err) {
32
+ return errorResult(err);
33
+ }
34
+ },
35
+ },
36
+ { name: spec.name },
37
+ );
38
+ }
39
+
40
+ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
41
+ if (!api.config) {
42
+ api.logger.debug?.("feishu_wiki: No config available, skipping wiki tools");
43
+ return;
44
+ }
45
+
46
+ if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
47
+ api.logger.debug?.("feishu_wiki: No Feishu accounts configured, skipping wiki tools");
48
+ return;
49
+ }
50
+
51
+ if (!hasFeishuToolEnabledForAnyAccount(api.config, "wiki")) {
52
+ api.logger.debug?.("feishu_wiki: wiki tool disabled in config");
53
+ return;
54
+ }
55
+
56
+ registerWikiTool<FeishuWikiParams>(api, {
57
+ name: "feishu_wiki",
58
+ label: "Feishu Wiki",
59
+ description:
60
+ "Feishu knowledge base operations. Actions: spaces, nodes, get, create, move, rename",
61
+ parameters: FeishuWikiSchema,
62
+ run: (client, params) => runWikiAction(client, params),
63
+ });
64
+
65
+ api.logger.debug?.("feishu_wiki: Registered feishu_wiki tool");
66
+ }
package/src/bitable.ts DELETED
@@ -1 +0,0 @@
1
- export { registerFeishuBitableTools } from "./bitable-tools/register.js";
package/src/docx.ts DELETED
@@ -1,276 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
- import type * as Lark from "@larksuiteoapi/node-sdk";
4
- import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
5
- import { appendDoc, createAndWriteDoc, createDoc, writeDoc } from "./doc-write-service.js";
6
- import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "./tools-common/tool-exec.js";
7
-
8
- // ============ Helpers ============
9
-
10
- function json(data: unknown) {
11
- return {
12
- content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
13
- details: data,
14
- };
15
- }
16
-
17
- const BLOCK_TYPE_NAMES: Record<number, string> = {
18
- 1: "Page",
19
- 2: "Text",
20
- 3: "Heading1",
21
- 4: "Heading2",
22
- 5: "Heading3",
23
- 12: "Bullet",
24
- 13: "Ordered",
25
- 14: "Code",
26
- 15: "Quote",
27
- 17: "Todo",
28
- 18: "Bitable",
29
- 21: "Diagram",
30
- 22: "Divider",
31
- 23: "File",
32
- 27: "Image",
33
- 30: "Sheet",
34
- 31: "Table",
35
- 32: "TableCell",
36
- };
37
-
38
- const STRUCTURED_BLOCK_TYPES = new Set([14, 18, 21, 23, 27, 30, 31, 32]);
39
-
40
- // ============ Actions ============
41
-
42
- async function readDoc(client: Lark.Client, docToken: string) {
43
- const [contentRes, infoRes, blocksRes] = await Promise.all([
44
- client.docx.document.rawContent({ path: { document_id: docToken } }),
45
- client.docx.document.get({ path: { document_id: docToken } }),
46
- client.docx.documentBlock.list({ path: { document_id: docToken } }),
47
- ]);
48
-
49
- if (contentRes.code !== 0) throw new Error(contentRes.msg);
50
-
51
- const blocks = blocksRes.data?.items ?? [];
52
- const blockCounts: Record<string, number> = {};
53
- const structuredTypes: string[] = [];
54
-
55
- for (const b of blocks) {
56
- const type = b.block_type ?? 0;
57
- const name = BLOCK_TYPE_NAMES[type] || `type_${type}`;
58
- blockCounts[name] = (blockCounts[name] || 0) + 1;
59
-
60
- if (STRUCTURED_BLOCK_TYPES.has(type) && !structuredTypes.includes(name)) {
61
- structuredTypes.push(name);
62
- }
63
- }
64
-
65
- let hint: string | undefined;
66
- if (structuredTypes.length > 0) {
67
- hint = `This document contains ${structuredTypes.join(", ")} which are NOT included in the plain text above. Use feishu_doc with action: "list_blocks" to get full content.`;
68
- }
69
-
70
- return {
71
- title: infoRes.data?.document?.title,
72
- content: contentRes.data?.content,
73
- revision_id: infoRes.data?.document?.revision_id,
74
- block_count: blocks.length,
75
- block_types: blockCounts,
76
- ...(hint && { hint }),
77
- };
78
- }
79
-
80
- async function updateBlock(
81
- client: Lark.Client,
82
- docToken: string,
83
- blockId: string,
84
- content: string,
85
- ) {
86
- const blockInfo = await client.docx.documentBlock.get({
87
- path: { document_id: docToken, block_id: blockId },
88
- });
89
- if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
90
-
91
- const res = await client.docx.documentBlock.patch({
92
- path: { document_id: docToken, block_id: blockId },
93
- data: {
94
- update_text_elements: {
95
- elements: [{ text_run: { content } }],
96
- },
97
- },
98
- });
99
- if (res.code !== 0) throw new Error(res.msg);
100
-
101
- return { success: true, block_id: blockId };
102
- }
103
-
104
- async function deleteBlock(client: Lark.Client, docToken: string, blockId: string) {
105
- const blockInfo = await client.docx.documentBlock.get({
106
- path: { document_id: docToken, block_id: blockId },
107
- });
108
- if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
109
-
110
- const parentId = blockInfo.data?.block?.parent_id ?? docToken;
111
-
112
- const children = await client.docx.documentBlockChildren.get({
113
- path: { document_id: docToken, block_id: parentId },
114
- });
115
- if (children.code !== 0) throw new Error(children.msg);
116
-
117
- const items = children.data?.items ?? [];
118
- const index = items.findIndex((item: any) => item.block_id === blockId);
119
- if (index === -1) throw new Error("Block not found");
120
-
121
- const res = await client.docx.documentBlockChildren.batchDelete({
122
- path: { document_id: docToken, block_id: parentId },
123
- data: { start_index: index, end_index: index + 1 },
124
- });
125
- if (res.code !== 0) throw new Error(res.msg);
126
-
127
- return { success: true, deleted_block_id: blockId };
128
- }
129
-
130
- async function listBlocks(client: Lark.Client, docToken: string) {
131
- const res = await client.docx.documentBlock.list({
132
- path: { document_id: docToken },
133
- });
134
- if (res.code !== 0) throw new Error(res.msg);
135
-
136
- return {
137
- blocks: res.data?.items ?? [],
138
- };
139
- }
140
-
141
- async function getBlock(client: Lark.Client, docToken: string, blockId: string) {
142
- const res = await client.docx.documentBlock.get({
143
- path: { document_id: docToken, block_id: blockId },
144
- });
145
- if (res.code !== 0) throw new Error(res.msg);
146
-
147
- return {
148
- block: res.data?.block,
149
- };
150
- }
151
-
152
- async function listAppScopes(client: Lark.Client) {
153
- const res = await client.application.scope.list({});
154
- if (res.code !== 0) throw new Error(res.msg);
155
-
156
- const scopes = res.data?.scopes ?? [];
157
- const granted = scopes.filter((s) => s.grant_status === 1);
158
- const pending = scopes.filter((s) => s.grant_status !== 1);
159
-
160
- return {
161
- granted: granted.map((s) => ({ name: s.scope_name, type: s.scope_type })),
162
- pending: pending.map((s) => ({ name: s.scope_name, type: s.scope_type })),
163
- summary: `${granted.length} granted, ${pending.length} pending`,
164
- };
165
- }
166
-
167
- // ============ Tool Registration ============
168
-
169
- export function registerFeishuDocTools(api: OpenClawPluginApi) {
170
- if (!api.config) {
171
- api.logger.debug?.("feishu_doc: No config available, skipping doc tools");
172
- return;
173
- }
174
-
175
- if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
176
- api.logger.debug?.("feishu_doc: No Feishu accounts configured, skipping doc tools");
177
- return;
178
- }
179
-
180
- // Registration happens once; account selection happens per execute() call.
181
- const docEnabled = hasFeishuToolEnabledForAnyAccount(api.config, "doc");
182
- const scopesEnabled = hasFeishuToolEnabledForAnyAccount(api.config, "scopes");
183
- const registered: string[] = [];
184
-
185
- // Main document tool with action-based dispatch
186
- if (docEnabled) {
187
- api.registerTool(
188
- {
189
- name: "feishu_doc",
190
- label: "Feishu Doc",
191
- description:
192
- 'Feishu document operations. Actions: read, write, append, create, create_and_write, list_blocks, get_block, update_block, delete_block. Use "create_and_write" for atomic create + content write.',
193
- parameters: FeishuDocSchema,
194
- async execute(_toolCallId, params) {
195
- const p = params as FeishuDocParams;
196
- try {
197
- return await withFeishuToolClient({
198
- api,
199
- toolName: "feishu_doc",
200
- requiredTool: "doc",
201
- run: async ({ client, account }) => {
202
- const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
203
- switch (p.action) {
204
- case "read":
205
- return json(await readDoc(client, p.doc_token));
206
- case "write":
207
- return json(await writeDoc(client, p.doc_token, p.content, mediaMaxBytes));
208
- case "append":
209
- return json(await appendDoc(client, p.doc_token, p.content, mediaMaxBytes));
210
- case "create":
211
- return json(await createDoc(client, p.title, p.folder_token));
212
- case "create_and_write":
213
- return json(
214
- await createAndWriteDoc(
215
- client,
216
- p.title,
217
- p.content,
218
- mediaMaxBytes,
219
- p.folder_token,
220
- ),
221
- );
222
- case "list_blocks":
223
- return json(await listBlocks(client, p.doc_token));
224
- case "get_block":
225
- return json(await getBlock(client, p.doc_token, p.block_id));
226
- case "update_block":
227
- return json(await updateBlock(client, p.doc_token, p.block_id, p.content));
228
- case "delete_block":
229
- return json(await deleteBlock(client, p.doc_token, p.block_id));
230
- default:
231
- return json({ error: `Unknown action: ${(p as any).action}` });
232
- }
233
- },
234
- });
235
- } catch (err) {
236
- return json({ error: err instanceof Error ? err.message : String(err) });
237
- }
238
- },
239
- },
240
- { name: "feishu_doc" },
241
- );
242
- registered.push("feishu_doc");
243
- }
244
-
245
- // Keep feishu_app_scopes as independent tool
246
- if (scopesEnabled) {
247
- api.registerTool(
248
- {
249
- name: "feishu_app_scopes",
250
- label: "Feishu App Scopes",
251
- description:
252
- "List current app permissions (scopes). Use to debug permission issues or check available capabilities.",
253
- parameters: Type.Object({}),
254
- async execute() {
255
- try {
256
- const result = await withFeishuToolClient({
257
- api,
258
- toolName: "feishu_app_scopes",
259
- requiredTool: "scopes",
260
- run: async ({ client }) => listAppScopes(client),
261
- });
262
- return json(result);
263
- } catch (err) {
264
- return json({ error: err instanceof Error ? err.message : String(err) });
265
- }
266
- },
267
- },
268
- { name: "feishu_app_scopes" },
269
- );
270
- registered.push("feishu_app_scopes");
271
- }
272
-
273
- if (registered.length > 0) {
274
- api.logger.debug?.(`feishu_doc: Registered ${registered.join(", ")}`);
275
- }
276
- }