@max1874/feishu 0.2.0 → 0.2.2
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/README.md +63 -17
- package/index.ts +17 -3
- package/{clawdbot.plugin.json → openclaw.plugin.json} +1 -0
- package/package.json +8 -7
- package/src/accounts.ts +2 -2
- package/src/bot.ts +158 -11
- package/src/channel.ts +2 -2
- package/src/directory.ts +1 -1
- package/src/docx.ts +967 -0
- package/src/media.ts +1 -1
- package/src/mention.ts +121 -0
- package/src/monitor.ts +1 -1
- package/src/onboarding.ts +2 -2
- package/src/outbound.ts +1 -1
- package/src/policy.ts +1 -1
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.ts +12 -2
- package/src/runtime.ts +1 -1
- package/src/send.ts +125 -5
- package/src/types.ts +5 -0
- package/src/typing.ts +1 -1
package/src/docx.ts
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import { createFeishuClient } from "./client.js";
|
|
4
|
+
import type { FeishuConfig } from "./types.js";
|
|
5
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
6
|
+
import { Readable } from "stream";
|
|
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
|
+
function extractBlockPreview(block: any): string {
|
|
18
|
+
const elements =
|
|
19
|
+
block.text?.elements ??
|
|
20
|
+
block.heading1?.elements ??
|
|
21
|
+
block.heading2?.elements ??
|
|
22
|
+
block.heading3?.elements ??
|
|
23
|
+
[];
|
|
24
|
+
return elements
|
|
25
|
+
.filter((e: any) => e.text_run)
|
|
26
|
+
.map((e: any) => e.text_run.content)
|
|
27
|
+
.join("")
|
|
28
|
+
.slice(0, 50);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extract image URLs from markdown content */
|
|
32
|
+
function extractImageUrls(markdown: string): string[] {
|
|
33
|
+
const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
34
|
+
const urls: string[] = [];
|
|
35
|
+
let match;
|
|
36
|
+
while ((match = regex.exec(markdown)) !== null) {
|
|
37
|
+
const url = match[1].trim();
|
|
38
|
+
// Only collect http(s) URLs, not file paths
|
|
39
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
40
|
+
urls.push(url);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return urls;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const BLOCK_TYPE_NAMES: Record<number, string> = {
|
|
47
|
+
1: "Page",
|
|
48
|
+
2: "Text",
|
|
49
|
+
3: "Heading1",
|
|
50
|
+
4: "Heading2",
|
|
51
|
+
5: "Heading3",
|
|
52
|
+
12: "Bullet",
|
|
53
|
+
13: "Ordered",
|
|
54
|
+
14: "Code",
|
|
55
|
+
15: "Quote",
|
|
56
|
+
17: "Todo",
|
|
57
|
+
18: "Bitable",
|
|
58
|
+
21: "Diagram",
|
|
59
|
+
22: "Divider",
|
|
60
|
+
23: "File",
|
|
61
|
+
27: "Image",
|
|
62
|
+
30: "Sheet",
|
|
63
|
+
31: "Table",
|
|
64
|
+
32: "TableCell",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Block types that cannot be created via documentBlockChildren.create API
|
|
68
|
+
// Note: Table (31) and TableCell (32) were previously excluded, but the convert API
|
|
69
|
+
// returns them with proper structure, so we attempt to create them directly.
|
|
70
|
+
// If creation fails, the error will be reported to the user.
|
|
71
|
+
const UNSUPPORTED_CREATE_TYPES = new Set<number>([
|
|
72
|
+
// Empty for now - let's try creating tables directly
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/** Clean blocks for insertion (remove unsupported types and read-only fields) */
|
|
76
|
+
function cleanBlocksForInsert(blocks: any[]): { cleaned: any[]; skipped: string[] } {
|
|
77
|
+
const skipped: string[] = [];
|
|
78
|
+
const cleaned = blocks
|
|
79
|
+
.filter((block) => {
|
|
80
|
+
if (UNSUPPORTED_CREATE_TYPES.has(block.block_type)) {
|
|
81
|
+
const typeName = BLOCK_TYPE_NAMES[block.block_type] || `type_${block.block_type}`;
|
|
82
|
+
skipped.push(typeName);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
})
|
|
87
|
+
.map((block) => {
|
|
88
|
+
// Remove read-only fields from table blocks that might cause API errors
|
|
89
|
+
if (block.block_type === 31 && block.table?.merge_info) {
|
|
90
|
+
const { merge_info, ...tableRest } = block.table;
|
|
91
|
+
return { ...block, table: tableRest };
|
|
92
|
+
}
|
|
93
|
+
return block;
|
|
94
|
+
});
|
|
95
|
+
return { cleaned, skipped };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============ Core Functions ============
|
|
99
|
+
|
|
100
|
+
/** Convert markdown to Feishu blocks using the Convert API */
|
|
101
|
+
async function convertMarkdown(client: Lark.Client, markdown: string) {
|
|
102
|
+
const res = await client.docx.document.convert({
|
|
103
|
+
data: { content_type: "markdown", content: markdown },
|
|
104
|
+
});
|
|
105
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
106
|
+
return {
|
|
107
|
+
blocks: res.data?.blocks ?? [],
|
|
108
|
+
firstLevelBlockIds: res.data?.first_level_block_ids ?? [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Insert blocks as children of a parent block */
|
|
113
|
+
async function insertBlocks(
|
|
114
|
+
client: Lark.Client,
|
|
115
|
+
docToken: string,
|
|
116
|
+
blocks: any[],
|
|
117
|
+
parentBlockId?: string,
|
|
118
|
+
): Promise<{ children: any[]; skipped: string[] }> {
|
|
119
|
+
const { cleaned, skipped } = cleanBlocksForInsert(blocks);
|
|
120
|
+
const blockId = parentBlockId ?? docToken;
|
|
121
|
+
|
|
122
|
+
if (cleaned.length === 0) {
|
|
123
|
+
return { children: [], skipped };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const res = await client.docx.documentBlockChildren.create({
|
|
127
|
+
path: { document_id: docToken, block_id: blockId },
|
|
128
|
+
data: { children: cleaned },
|
|
129
|
+
});
|
|
130
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
131
|
+
return { children: res.data?.children ?? [], skipped };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Delete all child blocks from a parent */
|
|
135
|
+
async function clearDocumentContent(client: Lark.Client, docToken: string) {
|
|
136
|
+
const existing = await client.docx.documentBlock.list({
|
|
137
|
+
path: { document_id: docToken },
|
|
138
|
+
});
|
|
139
|
+
if (existing.code !== 0) throw new Error(existing.msg);
|
|
140
|
+
|
|
141
|
+
const childIds =
|
|
142
|
+
existing.data?.items
|
|
143
|
+
?.filter((b) => b.parent_id === docToken && b.block_type !== 1)
|
|
144
|
+
.map((b) => b.block_id) ?? [];
|
|
145
|
+
|
|
146
|
+
if (childIds.length > 0) {
|
|
147
|
+
const res = await client.docx.documentBlockChildren.batchDelete({
|
|
148
|
+
path: { document_id: docToken, block_id: docToken },
|
|
149
|
+
data: { start_index: 0, end_index: childIds.length },
|
|
150
|
+
});
|
|
151
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return childIds.length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Upload image to Feishu drive for docx */
|
|
158
|
+
async function uploadImageToDocx(
|
|
159
|
+
client: Lark.Client,
|
|
160
|
+
blockId: string,
|
|
161
|
+
imageBuffer: Buffer,
|
|
162
|
+
fileName: string,
|
|
163
|
+
): Promise<string> {
|
|
164
|
+
const res = await client.drive.media.uploadAll({
|
|
165
|
+
data: {
|
|
166
|
+
file_name: fileName,
|
|
167
|
+
parent_type: "docx_image",
|
|
168
|
+
parent_node: blockId,
|
|
169
|
+
size: imageBuffer.length,
|
|
170
|
+
file: Readable.from(imageBuffer) as any,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const fileToken = res?.file_token;
|
|
175
|
+
if (!fileToken) {
|
|
176
|
+
throw new Error("Image upload failed: no file_token returned");
|
|
177
|
+
}
|
|
178
|
+
return fileToken;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Download image from URL */
|
|
182
|
+
async function downloadImage(url: string): Promise<Buffer> {
|
|
183
|
+
const response = await fetch(url);
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
|
|
186
|
+
}
|
|
187
|
+
return Buffer.from(await response.arrayBuffer());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Process images in markdown: download from URL, upload to Feishu, update blocks */
|
|
191
|
+
async function processImages(
|
|
192
|
+
client: Lark.Client,
|
|
193
|
+
docToken: string,
|
|
194
|
+
markdown: string,
|
|
195
|
+
insertedBlocks: any[],
|
|
196
|
+
): Promise<number> {
|
|
197
|
+
const imageUrls = extractImageUrls(markdown);
|
|
198
|
+
if (imageUrls.length === 0) return 0;
|
|
199
|
+
|
|
200
|
+
// Find Image blocks (block_type 27)
|
|
201
|
+
const imageBlocks = insertedBlocks.filter((b) => b.block_type === 27);
|
|
202
|
+
|
|
203
|
+
let processed = 0;
|
|
204
|
+
for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
|
|
205
|
+
const url = imageUrls[i];
|
|
206
|
+
const blockId = imageBlocks[i].block_id;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Download image from URL
|
|
210
|
+
const buffer = await downloadImage(url);
|
|
211
|
+
|
|
212
|
+
// Generate filename from URL
|
|
213
|
+
const urlPath = new URL(url).pathname;
|
|
214
|
+
const fileName = urlPath.split("/").pop() || `image_${i}.png`;
|
|
215
|
+
|
|
216
|
+
// Upload to Feishu
|
|
217
|
+
const fileToken = await uploadImageToDocx(client, blockId, buffer, fileName);
|
|
218
|
+
|
|
219
|
+
// Update the image block
|
|
220
|
+
await client.docx.documentBlock.patch({
|
|
221
|
+
path: { document_id: docToken, block_id: blockId },
|
|
222
|
+
data: {
|
|
223
|
+
replace_image: { token: fileToken },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
processed++;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
// Log but continue processing other images
|
|
230
|
+
console.error(`Failed to process image ${url}:`, err);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return processed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============ Wiki Functions ============
|
|
238
|
+
|
|
239
|
+
type WikiNodeInfo = {
|
|
240
|
+
node_token: string;
|
|
241
|
+
obj_token: string;
|
|
242
|
+
obj_type: string;
|
|
243
|
+
space_id: string;
|
|
244
|
+
title?: string;
|
|
245
|
+
parent_node_token?: string;
|
|
246
|
+
node_type?: string;
|
|
247
|
+
origin_space_id?: string;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** Get wiki node info by token (from /wiki/XXX URL) */
|
|
251
|
+
async function getWikiNode(client: Lark.Client, wikiToken: string): Promise<WikiNodeInfo> {
|
|
252
|
+
const res = await client.wiki.space.getNode({
|
|
253
|
+
params: { token: wikiToken },
|
|
254
|
+
});
|
|
255
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
256
|
+
const node = res.data?.node;
|
|
257
|
+
if (!node) throw new Error("Wiki node not found");
|
|
258
|
+
return {
|
|
259
|
+
node_token: node.node_token ?? wikiToken,
|
|
260
|
+
obj_token: node.obj_token ?? "",
|
|
261
|
+
obj_type: node.obj_type ?? "",
|
|
262
|
+
space_id: node.space_id ?? "",
|
|
263
|
+
title: node.title,
|
|
264
|
+
parent_node_token: node.parent_node_token,
|
|
265
|
+
node_type: node.node_type,
|
|
266
|
+
origin_space_id: node.origin_space_id,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Read wiki page content (resolves to underlying docx) */
|
|
271
|
+
async function readWiki(client: Lark.Client, wikiToken: string) {
|
|
272
|
+
// 1. Get wiki node info to find the underlying document
|
|
273
|
+
const node = await getWikiNode(client, wikiToken);
|
|
274
|
+
|
|
275
|
+
if (node.obj_type !== "docx") {
|
|
276
|
+
return {
|
|
277
|
+
node,
|
|
278
|
+
error: `Wiki node is of type '${node.obj_type}', only 'docx' is supported for reading content. Use the obj_token with the appropriate API.`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 2. Read the underlying docx content
|
|
283
|
+
const docContent = await readDoc(client, node.obj_token);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
wiki_token: wikiToken,
|
|
287
|
+
title: node.title,
|
|
288
|
+
obj_type: node.obj_type,
|
|
289
|
+
obj_token: node.obj_token,
|
|
290
|
+
space_id: node.space_id,
|
|
291
|
+
...docContent,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** List wiki spaces */
|
|
296
|
+
async function listWikiSpaces(client: Lark.Client) {
|
|
297
|
+
const res = await client.wiki.space.list({});
|
|
298
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
spaces:
|
|
302
|
+
res.data?.items?.map((s) => ({
|
|
303
|
+
space_id: s.space_id,
|
|
304
|
+
name: s.name,
|
|
305
|
+
description: s.description,
|
|
306
|
+
type: s.space_type === "team" ? "team" : "personal",
|
|
307
|
+
visibility: s.visibility,
|
|
308
|
+
})) ?? [],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** List wiki nodes under a parent */
|
|
313
|
+
async function listWikiNodes(client: Lark.Client, spaceId: string, parentNodeToken?: string) {
|
|
314
|
+
const res = await client.wiki.spaceNode.list({
|
|
315
|
+
path: { space_id: spaceId },
|
|
316
|
+
params: { parent_node_token: parentNodeToken },
|
|
317
|
+
});
|
|
318
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
nodes:
|
|
322
|
+
res.data?.items?.map((n) => ({
|
|
323
|
+
node_token: n.node_token,
|
|
324
|
+
obj_token: n.obj_token,
|
|
325
|
+
obj_type: n.obj_type,
|
|
326
|
+
title: n.title,
|
|
327
|
+
has_child: n.has_child,
|
|
328
|
+
})) ?? [],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ============ Actions ============
|
|
333
|
+
|
|
334
|
+
// Block types that are NOT included in rawContent (plain text) output
|
|
335
|
+
const STRUCTURED_BLOCK_TYPES = new Set([14, 18, 21, 23, 27, 30, 31, 32]);
|
|
336
|
+
// 14=Code, 18=Bitable, 21=Diagram, 23=File, 27=Image, 30=Sheet, 31=Table, 32=TableCell
|
|
337
|
+
|
|
338
|
+
async function readDoc(client: Lark.Client, docToken: string) {
|
|
339
|
+
const [contentRes, infoRes, blocksRes] = await Promise.all([
|
|
340
|
+
client.docx.document.rawContent({ path: { document_id: docToken } }),
|
|
341
|
+
client.docx.document.get({ path: { document_id: docToken } }),
|
|
342
|
+
client.docx.documentBlock.list({ path: { document_id: docToken } }),
|
|
343
|
+
]);
|
|
344
|
+
|
|
345
|
+
if (contentRes.code !== 0) throw new Error(contentRes.msg);
|
|
346
|
+
|
|
347
|
+
const blocks = blocksRes.data?.items ?? [];
|
|
348
|
+
const blockCounts: Record<string, number> = {};
|
|
349
|
+
const structuredTypes: string[] = [];
|
|
350
|
+
|
|
351
|
+
for (const b of blocks) {
|
|
352
|
+
const type = b.block_type ?? 0;
|
|
353
|
+
const name = BLOCK_TYPE_NAMES[type] || `type_${type}`;
|
|
354
|
+
blockCounts[name] = (blockCounts[name] || 0) + 1;
|
|
355
|
+
|
|
356
|
+
// Track structured types that need list_blocks to read
|
|
357
|
+
if (STRUCTURED_BLOCK_TYPES.has(type) && !structuredTypes.includes(name)) {
|
|
358
|
+
structuredTypes.push(name);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Build hint if there are structured blocks
|
|
363
|
+
let hint: string | undefined;
|
|
364
|
+
if (structuredTypes.length > 0) {
|
|
365
|
+
hint = `This document contains ${structuredTypes.join(", ")} which are NOT included in the plain text above. Use feishu_doc_list_blocks to get full content.`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
title: infoRes.data?.document?.title,
|
|
370
|
+
content: contentRes.data?.content,
|
|
371
|
+
revision_id: infoRes.data?.document?.revision_id,
|
|
372
|
+
block_count: blocks.length,
|
|
373
|
+
block_types: blockCounts,
|
|
374
|
+
...(hint && { hint }),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Set document permission to allow tenant members to edit */
|
|
379
|
+
async function setDocPermissionTenantEditable(client: Lark.Client, docToken: string) {
|
|
380
|
+
const res = await client.drive.permissionPublic.patch({
|
|
381
|
+
path: { token: docToken },
|
|
382
|
+
params: { type: "docx" },
|
|
383
|
+
data: {
|
|
384
|
+
link_share_entity: "tenant_editable",
|
|
385
|
+
share_entity: "same_tenant",
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
389
|
+
return res.data;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export type DocPermission = "private" | "tenant_editable";
|
|
393
|
+
|
|
394
|
+
async function createDoc(
|
|
395
|
+
client: Lark.Client,
|
|
396
|
+
title: string,
|
|
397
|
+
folderToken?: string,
|
|
398
|
+
permission?: DocPermission,
|
|
399
|
+
) {
|
|
400
|
+
const res = await client.docx.document.create({
|
|
401
|
+
data: { title, folder_token: folderToken },
|
|
402
|
+
});
|
|
403
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
404
|
+
const doc = res.data?.document;
|
|
405
|
+
const docId = doc?.document_id;
|
|
406
|
+
|
|
407
|
+
// Set permission if requested
|
|
408
|
+
if (permission === "tenant_editable" && docId) {
|
|
409
|
+
await setDocPermissionTenantEditable(client, docId);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
document_id: docId,
|
|
414
|
+
title: doc?.title,
|
|
415
|
+
url: `https://feishu.cn/docx/${docId}`,
|
|
416
|
+
permission: permission ?? "private",
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function writeDoc(client: Lark.Client, docToken: string, markdown: string) {
|
|
421
|
+
// 1. Clear existing content
|
|
422
|
+
const deleted = await clearDocumentContent(client, docToken);
|
|
423
|
+
|
|
424
|
+
// 2. Convert markdown to blocks
|
|
425
|
+
const { blocks } = await convertMarkdown(client, markdown);
|
|
426
|
+
if (blocks.length === 0) {
|
|
427
|
+
return { success: true, blocks_deleted: deleted, blocks_added: 0, images_processed: 0 };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 3. Insert new blocks (unsupported types like Table are filtered)
|
|
431
|
+
const { children: inserted, skipped } = await insertBlocks(client, docToken, blocks);
|
|
432
|
+
|
|
433
|
+
// 4. Process images
|
|
434
|
+
const imagesProcessed = await processImages(client, docToken, markdown, inserted);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
success: true,
|
|
438
|
+
blocks_deleted: deleted,
|
|
439
|
+
blocks_added: inserted.length,
|
|
440
|
+
images_processed: imagesProcessed,
|
|
441
|
+
...(skipped.length > 0 && {
|
|
442
|
+
warning: `Skipped unsupported block types: ${skipped.join(", ")}. Tables are not supported via this API.`,
|
|
443
|
+
}),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function appendDoc(client: Lark.Client, docToken: string, markdown: string) {
|
|
448
|
+
// 1. Convert markdown to blocks
|
|
449
|
+
const { blocks } = await convertMarkdown(client, markdown);
|
|
450
|
+
if (blocks.length === 0) {
|
|
451
|
+
throw new Error("Content is empty");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 2. Insert blocks (unsupported types like Table are filtered)
|
|
455
|
+
const { children: inserted, skipped } = await insertBlocks(client, docToken, blocks);
|
|
456
|
+
|
|
457
|
+
// 3. Process images
|
|
458
|
+
const imagesProcessed = await processImages(client, docToken, markdown, inserted);
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
success: true,
|
|
462
|
+
blocks_added: inserted.length,
|
|
463
|
+
images_processed: imagesProcessed,
|
|
464
|
+
block_ids: inserted.map((b: any) => b.block_id),
|
|
465
|
+
...(skipped.length > 0 && {
|
|
466
|
+
warning: `Skipped unsupported block types: ${skipped.join(", ")}. Tables are not supported via this API.`,
|
|
467
|
+
}),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function updateBlock(
|
|
472
|
+
client: Lark.Client,
|
|
473
|
+
docToken: string,
|
|
474
|
+
blockId: string,
|
|
475
|
+
content: string,
|
|
476
|
+
) {
|
|
477
|
+
const blockInfo = await client.docx.documentBlock.get({
|
|
478
|
+
path: { document_id: docToken, block_id: blockId },
|
|
479
|
+
});
|
|
480
|
+
if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
|
|
481
|
+
|
|
482
|
+
const res = await client.docx.documentBlock.patch({
|
|
483
|
+
path: { document_id: docToken, block_id: blockId },
|
|
484
|
+
data: {
|
|
485
|
+
update_text_elements: {
|
|
486
|
+
elements: [{ text_run: { content } }],
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
491
|
+
|
|
492
|
+
return { success: true, block_id: blockId };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function deleteBlock(client: Lark.Client, docToken: string, blockId: string) {
|
|
496
|
+
const blockInfo = await client.docx.documentBlock.get({
|
|
497
|
+
path: { document_id: docToken, block_id: blockId },
|
|
498
|
+
});
|
|
499
|
+
if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
|
|
500
|
+
|
|
501
|
+
const parentId = blockInfo.data?.block?.parent_id ?? docToken;
|
|
502
|
+
|
|
503
|
+
const children = await client.docx.documentBlockChildren.get({
|
|
504
|
+
path: { document_id: docToken, block_id: parentId },
|
|
505
|
+
});
|
|
506
|
+
if (children.code !== 0) throw new Error(children.msg);
|
|
507
|
+
|
|
508
|
+
const items = children.data?.items ?? [];
|
|
509
|
+
const index = items.findIndex((item: any) => item.block_id === blockId);
|
|
510
|
+
if (index === -1) throw new Error("Block not found");
|
|
511
|
+
|
|
512
|
+
const res = await client.docx.documentBlockChildren.batchDelete({
|
|
513
|
+
path: { document_id: docToken, block_id: parentId },
|
|
514
|
+
data: { start_index: index, end_index: index + 1 },
|
|
515
|
+
});
|
|
516
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
517
|
+
|
|
518
|
+
return { success: true, deleted_block_id: blockId };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function listBlocks(client: Lark.Client, docToken: string) {
|
|
522
|
+
const res = await client.docx.documentBlock.list({
|
|
523
|
+
path: { document_id: docToken },
|
|
524
|
+
});
|
|
525
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
526
|
+
|
|
527
|
+
// Return full block data for agent to parse
|
|
528
|
+
return {
|
|
529
|
+
blocks: res.data?.items ?? [],
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function getBlock(client: Lark.Client, docToken: string, blockId: string) {
|
|
534
|
+
const res = await client.docx.documentBlock.get({
|
|
535
|
+
path: { document_id: docToken, block_id: blockId },
|
|
536
|
+
});
|
|
537
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
block: res.data?.block,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function listFolder(client: Lark.Client, folderToken: string) {
|
|
545
|
+
const res = await client.drive.file.list({
|
|
546
|
+
params: { folder_token: folderToken },
|
|
547
|
+
});
|
|
548
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
files: res.data?.files?.map((f) => ({
|
|
552
|
+
token: f.token,
|
|
553
|
+
name: f.name,
|
|
554
|
+
type: f.type,
|
|
555
|
+
url: f.url,
|
|
556
|
+
})),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function listAppScopes(client: Lark.Client) {
|
|
561
|
+
const res = await client.application.scope.list({});
|
|
562
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
563
|
+
|
|
564
|
+
const scopes = res.data?.scopes ?? [];
|
|
565
|
+
const granted = scopes.filter((s) => s.grant_status === 1);
|
|
566
|
+
const pending = scopes.filter((s) => s.grant_status !== 1);
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
granted: granted.map((s) => ({ name: s.scope_name, type: s.scope_type })),
|
|
570
|
+
pending: pending.map((s) => ({ name: s.scope_name, type: s.scope_type })),
|
|
571
|
+
summary: `${granted.length} granted, ${pending.length} pending`,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ============ Schemas ============
|
|
576
|
+
|
|
577
|
+
const DocTokenSchema = Type.Object({
|
|
578
|
+
doc_token: Type.String({ description: "Document token (extract from URL /docx/XXX)" }),
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const CreateDocSchema = Type.Object({
|
|
582
|
+
title: Type.String({ description: "Document title" }),
|
|
583
|
+
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
|
|
584
|
+
permission: Type.Optional(
|
|
585
|
+
Type.Union([Type.Literal("private"), Type.Literal("tenant_editable")], {
|
|
586
|
+
description:
|
|
587
|
+
"Document permission: 'private' (default) or 'tenant_editable' (organization members can edit via link)",
|
|
588
|
+
}),
|
|
589
|
+
),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const WriteDocSchema = Type.Object({
|
|
593
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
594
|
+
content: Type.String({
|
|
595
|
+
description: "Markdown content to write (replaces entire document content)",
|
|
596
|
+
}),
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const AppendDocSchema = Type.Object({
|
|
600
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
601
|
+
content: Type.String({ description: "Markdown content to append to end of document" }),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const UpdateBlockSchema = Type.Object({
|
|
605
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
606
|
+
block_id: Type.String({ description: "Block ID (get from list_blocks)" }),
|
|
607
|
+
content: Type.String({ description: "New text content" }),
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const DeleteBlockSchema = Type.Object({
|
|
611
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
612
|
+
block_id: Type.String({ description: "Block ID" }),
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const GetBlockSchema = Type.Object({
|
|
616
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
617
|
+
block_id: Type.String({ description: "Block ID (from list_blocks)" }),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const FolderTokenSchema = Type.Object({
|
|
621
|
+
folder_token: Type.String({ description: "Folder token" }),
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const SetPermissionSchema = Type.Object({
|
|
625
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
626
|
+
permission: Type.Union([Type.Literal("private"), Type.Literal("tenant_editable")], {
|
|
627
|
+
description: "'private' or 'tenant_editable' (organization members can edit via link)",
|
|
628
|
+
}),
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Wiki Schemas
|
|
632
|
+
const WikiTokenSchema = Type.Object({
|
|
633
|
+
wiki_token: Type.String({ description: "Wiki token (extract from URL /wiki/XXX)" }),
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const WikiSpaceIdSchema = Type.Object({
|
|
637
|
+
space_id: Type.String({ description: "Wiki space ID" }),
|
|
638
|
+
parent_node_token: Type.Optional(
|
|
639
|
+
Type.String({ description: "Parent node token (optional, for listing children)" }),
|
|
640
|
+
),
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// ============ Tool Registration ============
|
|
644
|
+
|
|
645
|
+
export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
|
646
|
+
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
|
647
|
+
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
|
648
|
+
api.logger.debug?.("feishu_doc: Feishu credentials not configured, skipping doc tools");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const getClient = () => createFeishuClient(feishuCfg);
|
|
653
|
+
|
|
654
|
+
// Tool 1: feishu_doc_read
|
|
655
|
+
api.registerTool(
|
|
656
|
+
{
|
|
657
|
+
name: "feishu_doc_read",
|
|
658
|
+
label: "Feishu Doc Read",
|
|
659
|
+
description: "Read plain text content and metadata from a Feishu document",
|
|
660
|
+
parameters: DocTokenSchema,
|
|
661
|
+
async execute(_toolCallId, params) {
|
|
662
|
+
const { doc_token } = params as { doc_token: string };
|
|
663
|
+
try {
|
|
664
|
+
const result = await readDoc(getClient(), doc_token);
|
|
665
|
+
return json(result);
|
|
666
|
+
} catch (err) {
|
|
667
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
{ name: "feishu_doc_read" },
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// Tool 2: feishu_doc_create
|
|
675
|
+
api.registerTool(
|
|
676
|
+
{
|
|
677
|
+
name: "feishu_doc_create",
|
|
678
|
+
label: "Feishu Doc Create",
|
|
679
|
+
description:
|
|
680
|
+
"Create a new empty Feishu document. Use permission='tenant_editable' to allow organization members to edit via link.",
|
|
681
|
+
parameters: CreateDocSchema,
|
|
682
|
+
async execute(_toolCallId, params) {
|
|
683
|
+
const { title, folder_token, permission } = params as {
|
|
684
|
+
title: string;
|
|
685
|
+
folder_token?: string;
|
|
686
|
+
permission?: DocPermission;
|
|
687
|
+
};
|
|
688
|
+
try {
|
|
689
|
+
const result = await createDoc(getClient(), title, folder_token, permission);
|
|
690
|
+
return json(result);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
{ name: "feishu_doc_create" },
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
// Tool 3: feishu_doc_write (NEW)
|
|
700
|
+
api.registerTool(
|
|
701
|
+
{
|
|
702
|
+
name: "feishu_doc_write",
|
|
703
|
+
label: "Feishu Doc Write",
|
|
704
|
+
description:
|
|
705
|
+
"Write markdown content to a Feishu document (replaces all content). Supports headings, lists, code blocks, quotes, links, images, and text styling. Note: tables are not supported.",
|
|
706
|
+
parameters: WriteDocSchema,
|
|
707
|
+
async execute(_toolCallId, params) {
|
|
708
|
+
const { doc_token, content } = params as { doc_token: string; content: string };
|
|
709
|
+
try {
|
|
710
|
+
const result = await writeDoc(getClient(), doc_token, content);
|
|
711
|
+
return json(result);
|
|
712
|
+
} catch (err) {
|
|
713
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
{ name: "feishu_doc_write" },
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
// Tool 4: feishu_doc_append
|
|
721
|
+
api.registerTool(
|
|
722
|
+
{
|
|
723
|
+
name: "feishu_doc_append",
|
|
724
|
+
label: "Feishu Doc Append",
|
|
725
|
+
description:
|
|
726
|
+
"Append markdown content to the end of a Feishu document. Supports same markdown syntax as write.",
|
|
727
|
+
parameters: AppendDocSchema,
|
|
728
|
+
async execute(_toolCallId, params) {
|
|
729
|
+
const { doc_token, content } = params as { doc_token: string; content: string };
|
|
730
|
+
try {
|
|
731
|
+
const result = await appendDoc(getClient(), doc_token, content);
|
|
732
|
+
return json(result);
|
|
733
|
+
} catch (err) {
|
|
734
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
{ name: "feishu_doc_append" },
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// Tool 5: feishu_doc_update_block
|
|
742
|
+
api.registerTool(
|
|
743
|
+
{
|
|
744
|
+
name: "feishu_doc_update_block",
|
|
745
|
+
label: "Feishu Doc Update Block",
|
|
746
|
+
description: "Update the text content of a specific block in a Feishu document",
|
|
747
|
+
parameters: UpdateBlockSchema,
|
|
748
|
+
async execute(_toolCallId, params) {
|
|
749
|
+
const { doc_token, block_id, content } = params as {
|
|
750
|
+
doc_token: string;
|
|
751
|
+
block_id: string;
|
|
752
|
+
content: string;
|
|
753
|
+
};
|
|
754
|
+
try {
|
|
755
|
+
const result = await updateBlock(getClient(), doc_token, block_id, content);
|
|
756
|
+
return json(result);
|
|
757
|
+
} catch (err) {
|
|
758
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
{ name: "feishu_doc_update_block" },
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
// Tool 6: feishu_doc_delete_block
|
|
766
|
+
api.registerTool(
|
|
767
|
+
{
|
|
768
|
+
name: "feishu_doc_delete_block",
|
|
769
|
+
label: "Feishu Doc Delete Block",
|
|
770
|
+
description: "Delete a specific block from a Feishu document",
|
|
771
|
+
parameters: DeleteBlockSchema,
|
|
772
|
+
async execute(_toolCallId, params) {
|
|
773
|
+
const { doc_token, block_id } = params as { doc_token: string; block_id: string };
|
|
774
|
+
try {
|
|
775
|
+
const result = await deleteBlock(getClient(), doc_token, block_id);
|
|
776
|
+
return json(result);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
{ name: "feishu_doc_delete_block" },
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
// Tool 7: feishu_doc_list_blocks
|
|
786
|
+
api.registerTool(
|
|
787
|
+
{
|
|
788
|
+
name: "feishu_doc_list_blocks",
|
|
789
|
+
label: "Feishu Doc List Blocks",
|
|
790
|
+
description:
|
|
791
|
+
"List all blocks in a Feishu document with full content. Use this to read structured content like tables. Returns block_id for use with update/delete/get_block.",
|
|
792
|
+
parameters: DocTokenSchema,
|
|
793
|
+
async execute(_toolCallId, params) {
|
|
794
|
+
const { doc_token } = params as { doc_token: string };
|
|
795
|
+
try {
|
|
796
|
+
const result = await listBlocks(getClient(), doc_token);
|
|
797
|
+
return json(result);
|
|
798
|
+
} catch (err) {
|
|
799
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
{ name: "feishu_doc_list_blocks" },
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// Tool 8: feishu_doc_get_block
|
|
807
|
+
api.registerTool(
|
|
808
|
+
{
|
|
809
|
+
name: "feishu_doc_get_block",
|
|
810
|
+
label: "Feishu Doc Get Block",
|
|
811
|
+
description: "Get detailed content of a specific block by ID (from list_blocks)",
|
|
812
|
+
parameters: GetBlockSchema,
|
|
813
|
+
async execute(_toolCallId, params) {
|
|
814
|
+
const { doc_token, block_id } = params as { doc_token: string; block_id: string };
|
|
815
|
+
try {
|
|
816
|
+
const result = await getBlock(getClient(), doc_token, block_id);
|
|
817
|
+
return json(result);
|
|
818
|
+
} catch (err) {
|
|
819
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
{ name: "feishu_doc_get_block" },
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
// Tool 9: feishu_folder_list
|
|
827
|
+
api.registerTool(
|
|
828
|
+
{
|
|
829
|
+
name: "feishu_folder_list",
|
|
830
|
+
label: "Feishu Folder List",
|
|
831
|
+
description: "List documents and subfolders in a Feishu folder",
|
|
832
|
+
parameters: FolderTokenSchema,
|
|
833
|
+
async execute(_toolCallId, params) {
|
|
834
|
+
const { folder_token } = params as { folder_token: string };
|
|
835
|
+
try {
|
|
836
|
+
const result = await listFolder(getClient(), folder_token);
|
|
837
|
+
return json(result);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
{ name: "feishu_folder_list" },
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
// Tool 10: feishu_doc_set_permission
|
|
847
|
+
api.registerTool(
|
|
848
|
+
{
|
|
849
|
+
name: "feishu_doc_set_permission",
|
|
850
|
+
label: "Feishu Doc Set Permission",
|
|
851
|
+
description:
|
|
852
|
+
"Set document sharing permission. Use 'tenant_editable' to allow organization members to edit via link.",
|
|
853
|
+
parameters: SetPermissionSchema,
|
|
854
|
+
async execute(_toolCallId, params) {
|
|
855
|
+
const { doc_token, permission } = params as { doc_token: string; permission: DocPermission };
|
|
856
|
+
try {
|
|
857
|
+
if (permission === "tenant_editable") {
|
|
858
|
+
await setDocPermissionTenantEditable(getClient(), doc_token);
|
|
859
|
+
return json({ success: true, permission: "tenant_editable" });
|
|
860
|
+
} else {
|
|
861
|
+
// Reset to private (only owner can access)
|
|
862
|
+
const client = getClient();
|
|
863
|
+
const res = await client.drive.permissionPublic.patch({
|
|
864
|
+
path: { token: doc_token },
|
|
865
|
+
params: { type: "docx" },
|
|
866
|
+
data: {
|
|
867
|
+
link_share_entity: "closed",
|
|
868
|
+
share_entity: "only_full_access",
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
872
|
+
return json({ success: true, permission: "private" });
|
|
873
|
+
}
|
|
874
|
+
} catch (err) {
|
|
875
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
{ name: "feishu_doc_set_permission" },
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
// Tool 11: feishu_wiki_read
|
|
883
|
+
api.registerTool(
|
|
884
|
+
{
|
|
885
|
+
name: "feishu_wiki_read",
|
|
886
|
+
label: "Feishu Wiki Read",
|
|
887
|
+
description:
|
|
888
|
+
"Read content from a Feishu wiki page. Extract wiki_token from URL /wiki/XXX. Returns wiki metadata and document content if the underlying type is docx.",
|
|
889
|
+
parameters: WikiTokenSchema,
|
|
890
|
+
async execute(_toolCallId, params) {
|
|
891
|
+
const { wiki_token } = params as { wiki_token: string };
|
|
892
|
+
try {
|
|
893
|
+
const result = await readWiki(getClient(), wiki_token);
|
|
894
|
+
return json(result);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
{ name: "feishu_wiki_read" },
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// Tool 12: feishu_wiki_spaces
|
|
904
|
+
api.registerTool(
|
|
905
|
+
{
|
|
906
|
+
name: "feishu_wiki_spaces",
|
|
907
|
+
label: "Feishu Wiki Spaces",
|
|
908
|
+
description: "List available wiki spaces (knowledge bases) the app has access to.",
|
|
909
|
+
parameters: Type.Object({}),
|
|
910
|
+
async execute() {
|
|
911
|
+
try {
|
|
912
|
+
const result = await listWikiSpaces(getClient());
|
|
913
|
+
return json(result);
|
|
914
|
+
} catch (err) {
|
|
915
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
{ name: "feishu_wiki_spaces" },
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
// Tool 13: feishu_wiki_nodes
|
|
923
|
+
api.registerTool(
|
|
924
|
+
{
|
|
925
|
+
name: "feishu_wiki_nodes",
|
|
926
|
+
label: "Feishu Wiki Nodes",
|
|
927
|
+
description:
|
|
928
|
+
"List wiki nodes (pages) in a space. Use parent_node_token to list children of a specific node.",
|
|
929
|
+
parameters: WikiSpaceIdSchema,
|
|
930
|
+
async execute(_toolCallId, params) {
|
|
931
|
+
const { space_id, parent_node_token } = params as {
|
|
932
|
+
space_id: string;
|
|
933
|
+
parent_node_token?: string;
|
|
934
|
+
};
|
|
935
|
+
try {
|
|
936
|
+
const result = await listWikiNodes(getClient(), space_id, parent_node_token);
|
|
937
|
+
return json(result);
|
|
938
|
+
} catch (err) {
|
|
939
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
{ name: "feishu_wiki_nodes" },
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Tool 14: feishu_app_scopes
|
|
947
|
+
api.registerTool(
|
|
948
|
+
{
|
|
949
|
+
name: "feishu_app_scopes",
|
|
950
|
+
label: "Feishu App Scopes",
|
|
951
|
+
description:
|
|
952
|
+
"List current app permissions (scopes). Use to debug permission issues or check available capabilities.",
|
|
953
|
+
parameters: Type.Object({}),
|
|
954
|
+
async execute() {
|
|
955
|
+
try {
|
|
956
|
+
const result = await listAppScopes(getClient());
|
|
957
|
+
return json(result);
|
|
958
|
+
} catch (err) {
|
|
959
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
{ name: "feishu_app_scopes" },
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
api.logger.info?.(`feishu_doc: Registered 14 document/wiki tools`);
|
|
967
|
+
}
|