@m1heng-clawd/feishu 0.1.9 → 0.1.11
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 +199 -31
- package/index.ts +3 -1
- package/package.json +4 -4
- package/skills/feishu-doc/SKILL.md +24 -0
- package/src/bitable-tools/actions.ts +199 -0
- package/src/bitable-tools/common.ts +90 -0
- package/src/bitable-tools/meta.ts +80 -0
- package/src/bitable-tools/register.ts +195 -0
- package/src/bitable-tools/schemas.ts +221 -0
- package/src/bitable.ts +1 -441
- package/src/bot.ts +130 -68
- package/src/channel.ts +6 -8
- package/src/config-schema.ts +7 -0
- package/src/dedup.ts +31 -0
- package/src/doc-schema.ts +8 -0
- package/src/doc-write-service.ts +711 -0
- package/src/docx.ts +83 -287
- package/src/drive-schema.ts +20 -0
- package/src/drive.ts +57 -27
- package/src/dynamic-agent.ts +8 -4
- package/src/media.ts +25 -40
- package/src/monitor.ts +29 -2
- package/src/onboarding.ts +196 -105
- package/src/perm.ts +23 -24
- package/src/probe.ts +108 -4
- package/src/reply-dispatcher.ts +137 -73
- package/src/streaming-card.ts +211 -0
- package/src/targets.ts +1 -1
- package/src/task-tools/actions.ts +137 -0
- package/src/task-tools/common.ts +18 -0
- package/src/task-tools/register.ts +101 -0
- package/src/task-tools/schemas.ts +138 -0
- package/src/task.ts +1 -0
- package/src/tools-common/feishu-api.ts +184 -0
- package/src/tools-common/tool-context.ts +23 -0
- package/src/tools-common/tool-exec.ts +73 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/wiki.ts +42 -43
package/src/docx.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
-
import { createFeishuClient } from "./client.js";
|
|
4
|
-
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
|
|
5
3
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
6
|
-
import { Readable } from "stream";
|
|
7
4
|
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
|
8
|
-
import {
|
|
5
|
+
import { appendDoc, createAndWriteDoc, createDoc, writeDoc } from "./doc-write-service.js";
|
|
6
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "./tools-common/tool-exec.js";
|
|
9
7
|
|
|
10
8
|
// ============ Helpers ============
|
|
11
9
|
|
|
@@ -16,20 +14,6 @@ function json(data: unknown) {
|
|
|
16
14
|
};
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
/** Extract image URLs from markdown content */
|
|
20
|
-
function extractImageUrls(markdown: string): string[] {
|
|
21
|
-
const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
22
|
-
const urls: string[] = [];
|
|
23
|
-
let match;
|
|
24
|
-
while ((match = regex.exec(markdown)) !== null) {
|
|
25
|
-
const url = match[1].trim();
|
|
26
|
-
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
27
|
-
urls.push(url);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return urls;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
17
|
const BLOCK_TYPE_NAMES: Record<number, string> = {
|
|
34
18
|
1: "Page",
|
|
35
19
|
2: "Text",
|
|
@@ -51,160 +35,10 @@ const BLOCK_TYPE_NAMES: Record<number, string> = {
|
|
|
51
35
|
32: "TableCell",
|
|
52
36
|
};
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
const UNSUPPORTED_CREATE_TYPES = new Set([31, 32]);
|
|
56
|
-
|
|
57
|
-
/** Clean blocks for insertion (remove unsupported types and read-only fields) */
|
|
58
|
-
function cleanBlocksForInsert(blocks: any[]): { cleaned: any[]; skipped: string[] } {
|
|
59
|
-
const skipped: string[] = [];
|
|
60
|
-
const cleaned = blocks
|
|
61
|
-
.filter((block) => {
|
|
62
|
-
if (UNSUPPORTED_CREATE_TYPES.has(block.block_type)) {
|
|
63
|
-
const typeName = BLOCK_TYPE_NAMES[block.block_type] || `type_${block.block_type}`;
|
|
64
|
-
skipped.push(typeName);
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
return true;
|
|
68
|
-
})
|
|
69
|
-
.map((block) => {
|
|
70
|
-
if (block.block_type === 31 && block.table?.merge_info) {
|
|
71
|
-
const { merge_info, ...tableRest } = block.table;
|
|
72
|
-
return { ...block, table: tableRest };
|
|
73
|
-
}
|
|
74
|
-
return block;
|
|
75
|
-
});
|
|
76
|
-
return { cleaned, skipped };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ============ Core Functions ============
|
|
80
|
-
|
|
81
|
-
async function convertMarkdown(client: Lark.Client, markdown: string) {
|
|
82
|
-
const res = await client.docx.document.convert({
|
|
83
|
-
data: { content_type: "markdown", content: markdown },
|
|
84
|
-
});
|
|
85
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
86
|
-
return {
|
|
87
|
-
blocks: res.data?.blocks ?? [],
|
|
88
|
-
firstLevelBlockIds: res.data?.first_level_block_ids ?? [],
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function insertBlocks(
|
|
93
|
-
client: Lark.Client,
|
|
94
|
-
docToken: string,
|
|
95
|
-
blocks: any[],
|
|
96
|
-
parentBlockId?: string,
|
|
97
|
-
): Promise<{ children: any[]; skipped: string[] }> {
|
|
98
|
-
const { cleaned, skipped } = cleanBlocksForInsert(blocks);
|
|
99
|
-
const blockId = parentBlockId ?? docToken;
|
|
100
|
-
|
|
101
|
-
if (cleaned.length === 0) {
|
|
102
|
-
return { children: [], skipped };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const res = await client.docx.documentBlockChildren.create({
|
|
106
|
-
path: { document_id: docToken, block_id: blockId },
|
|
107
|
-
data: { children: cleaned },
|
|
108
|
-
});
|
|
109
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
110
|
-
return { children: res.data?.children ?? [], skipped };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function clearDocumentContent(client: Lark.Client, docToken: string) {
|
|
114
|
-
const existing = await client.docx.documentBlock.list({
|
|
115
|
-
path: { document_id: docToken },
|
|
116
|
-
});
|
|
117
|
-
if (existing.code !== 0) throw new Error(existing.msg);
|
|
118
|
-
|
|
119
|
-
const childIds =
|
|
120
|
-
existing.data?.items
|
|
121
|
-
?.filter((b) => b.parent_id === docToken && b.block_type !== 1)
|
|
122
|
-
.map((b) => b.block_id) ?? [];
|
|
123
|
-
|
|
124
|
-
if (childIds.length > 0) {
|
|
125
|
-
const res = await client.docx.documentBlockChildren.batchDelete({
|
|
126
|
-
path: { document_id: docToken, block_id: docToken },
|
|
127
|
-
data: { start_index: 0, end_index: childIds.length },
|
|
128
|
-
});
|
|
129
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return childIds.length;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function uploadImageToDocx(
|
|
136
|
-
client: Lark.Client,
|
|
137
|
-
blockId: string,
|
|
138
|
-
imageBuffer: Buffer,
|
|
139
|
-
fileName: string,
|
|
140
|
-
): Promise<string> {
|
|
141
|
-
const res = await client.drive.media.uploadAll({
|
|
142
|
-
data: {
|
|
143
|
-
file_name: fileName,
|
|
144
|
-
parent_type: "docx_image",
|
|
145
|
-
parent_node: blockId,
|
|
146
|
-
size: imageBuffer.length,
|
|
147
|
-
file: Readable.from(imageBuffer) as any,
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const fileToken = res?.file_token;
|
|
152
|
-
if (!fileToken) {
|
|
153
|
-
throw new Error("Image upload failed: no file_token returned");
|
|
154
|
-
}
|
|
155
|
-
return fileToken;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function downloadImage(url: string): Promise<Buffer> {
|
|
159
|
-
const response = await fetch(url);
|
|
160
|
-
if (!response.ok) {
|
|
161
|
-
throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
|
|
162
|
-
}
|
|
163
|
-
return Buffer.from(await response.arrayBuffer());
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function processImages(
|
|
167
|
-
client: Lark.Client,
|
|
168
|
-
docToken: string,
|
|
169
|
-
markdown: string,
|
|
170
|
-
insertedBlocks: any[],
|
|
171
|
-
): Promise<number> {
|
|
172
|
-
const imageUrls = extractImageUrls(markdown);
|
|
173
|
-
if (imageUrls.length === 0) return 0;
|
|
174
|
-
|
|
175
|
-
const imageBlocks = insertedBlocks.filter((b) => b.block_type === 27);
|
|
176
|
-
|
|
177
|
-
let processed = 0;
|
|
178
|
-
for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
|
|
179
|
-
const url = imageUrls[i];
|
|
180
|
-
const blockId = imageBlocks[i].block_id;
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const buffer = await downloadImage(url);
|
|
184
|
-
const urlPath = new URL(url).pathname;
|
|
185
|
-
const fileName = urlPath.split("/").pop() || `image_${i}.png`;
|
|
186
|
-
const fileToken = await uploadImageToDocx(client, blockId, buffer, fileName);
|
|
187
|
-
|
|
188
|
-
await client.docx.documentBlock.patch({
|
|
189
|
-
path: { document_id: docToken, block_id: blockId },
|
|
190
|
-
data: {
|
|
191
|
-
replace_image: { token: fileToken },
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
processed++;
|
|
196
|
-
} catch (err) {
|
|
197
|
-
console.error(`Failed to process image ${url}:`, err);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return processed;
|
|
202
|
-
}
|
|
38
|
+
const STRUCTURED_BLOCK_TYPES = new Set([14, 18, 21, 23, 27, 30, 31, 32]);
|
|
203
39
|
|
|
204
40
|
// ============ Actions ============
|
|
205
41
|
|
|
206
|
-
const STRUCTURED_BLOCK_TYPES = new Set([14, 18, 21, 23, 27, 30, 31, 32]);
|
|
207
|
-
|
|
208
42
|
async function readDoc(client: Lark.Client, docToken: string) {
|
|
209
43
|
const [contentRes, infoRes, blocksRes] = await Promise.all([
|
|
210
44
|
client.docx.document.rawContent({ path: { document_id: docToken } }),
|
|
@@ -243,61 +77,6 @@ async function readDoc(client: Lark.Client, docToken: string) {
|
|
|
243
77
|
};
|
|
244
78
|
}
|
|
245
79
|
|
|
246
|
-
async function createDoc(client: Lark.Client, title: string, folderToken?: string) {
|
|
247
|
-
const res = await client.docx.document.create({
|
|
248
|
-
data: { title, folder_token: folderToken },
|
|
249
|
-
});
|
|
250
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
251
|
-
const doc = res.data?.document;
|
|
252
|
-
return {
|
|
253
|
-
document_id: doc?.document_id,
|
|
254
|
-
title: doc?.title,
|
|
255
|
-
url: `https://feishu.cn/docx/${doc?.document_id}`,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function writeDoc(client: Lark.Client, docToken: string, markdown: string) {
|
|
260
|
-
const deleted = await clearDocumentContent(client, docToken);
|
|
261
|
-
|
|
262
|
-
const { blocks } = await convertMarkdown(client, markdown);
|
|
263
|
-
if (blocks.length === 0) {
|
|
264
|
-
return { success: true, blocks_deleted: deleted, blocks_added: 0, images_processed: 0 };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const { children: inserted, skipped } = await insertBlocks(client, docToken, blocks);
|
|
268
|
-
const imagesProcessed = await processImages(client, docToken, markdown, inserted);
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
success: true,
|
|
272
|
-
blocks_deleted: deleted,
|
|
273
|
-
blocks_added: inserted.length,
|
|
274
|
-
images_processed: imagesProcessed,
|
|
275
|
-
...(skipped.length > 0 && {
|
|
276
|
-
warning: `Skipped unsupported block types: ${skipped.join(", ")}. Tables are not supported via this API.`,
|
|
277
|
-
}),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function appendDoc(client: Lark.Client, docToken: string, markdown: string) {
|
|
282
|
-
const { blocks } = await convertMarkdown(client, markdown);
|
|
283
|
-
if (blocks.length === 0) {
|
|
284
|
-
throw new Error("Content is empty");
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const { children: inserted, skipped } = await insertBlocks(client, docToken, blocks);
|
|
288
|
-
const imagesProcessed = await processImages(client, docToken, markdown, inserted);
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
success: true,
|
|
292
|
-
blocks_added: inserted.length,
|
|
293
|
-
images_processed: imagesProcessed,
|
|
294
|
-
block_ids: inserted.map((b: any) => b.block_id),
|
|
295
|
-
...(skipped.length > 0 && {
|
|
296
|
-
warning: `Skipped unsupported block types: ${skipped.join(", ")}. Tables are not supported via this API.`,
|
|
297
|
-
}),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
80
|
async function updateBlock(
|
|
302
81
|
client: Lark.Client,
|
|
303
82
|
docToken: string,
|
|
@@ -393,88 +172,105 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
|
|
393
172
|
return;
|
|
394
173
|
}
|
|
395
174
|
|
|
396
|
-
|
|
397
|
-
const accounts = listEnabledFeishuAccounts(api.config);
|
|
398
|
-
if (accounts.length === 0) {
|
|
175
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
399
176
|
api.logger.debug?.("feishu_doc: No Feishu accounts configured, skipping doc tools");
|
|
400
177
|
return;
|
|
401
178
|
}
|
|
402
179
|
|
|
403
|
-
//
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
// Helper to get client for the default account
|
|
408
|
-
const getClient = () => createFeishuClient(firstAccount);
|
|
180
|
+
// Registration happens once; account selection happens per execute() call.
|
|
181
|
+
const docEnabled = hasFeishuToolEnabledForAnyAccount(api.config, "doc");
|
|
182
|
+
const scopesEnabled = hasFeishuToolEnabledForAnyAccount(api.config, "scopes");
|
|
409
183
|
const registered: string[] = [];
|
|
410
184
|
|
|
411
185
|
// Main document tool with action-based dispatch
|
|
412
|
-
if (
|
|
186
|
+
if (docEnabled) {
|
|
413
187
|
api.registerTool(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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) });
|
|
443
237
|
}
|
|
444
|
-
}
|
|
445
|
-
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
446
|
-
}
|
|
238
|
+
},
|
|
447
239
|
},
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
);
|
|
240
|
+
{ name: "feishu_doc" },
|
|
241
|
+
);
|
|
451
242
|
registered.push("feishu_doc");
|
|
452
243
|
}
|
|
453
244
|
|
|
454
245
|
// Keep feishu_app_scopes as independent tool
|
|
455
|
-
if (
|
|
246
|
+
if (scopesEnabled) {
|
|
456
247
|
api.registerTool(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
+
},
|
|
470
267
|
},
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
);
|
|
268
|
+
{ name: "feishu_app_scopes" },
|
|
269
|
+
);
|
|
474
270
|
registered.push("feishu_app_scopes");
|
|
475
271
|
}
|
|
476
272
|
|
|
477
273
|
if (registered.length > 0) {
|
|
478
|
-
api.logger.
|
|
274
|
+
api.logger.debug?.(`feishu_doc: Registered ${registered.join(", ")}`);
|
|
479
275
|
}
|
|
480
276
|
}
|
package/src/drive-schema.ts
CHANGED
|
@@ -11,6 +11,11 @@ const FileType = Type.Union([
|
|
|
11
11
|
Type.Literal("shortcut"),
|
|
12
12
|
]);
|
|
13
13
|
|
|
14
|
+
const DocType = Type.Union([
|
|
15
|
+
Type.Literal("docx", { description: "New generation document (default)" }),
|
|
16
|
+
Type.Literal("doc", { description: "Legacy document" }),
|
|
17
|
+
]);
|
|
18
|
+
|
|
14
19
|
export const FeishuDriveSchema = Type.Union([
|
|
15
20
|
Type.Object({
|
|
16
21
|
action: Type.Literal("list"),
|
|
@@ -41,6 +46,21 @@ export const FeishuDriveSchema = Type.Union([
|
|
|
41
46
|
file_token: Type.String({ description: "File token to delete" }),
|
|
42
47
|
type: FileType,
|
|
43
48
|
}),
|
|
49
|
+
Type.Object({
|
|
50
|
+
action: Type.Literal("import_document"),
|
|
51
|
+
title: Type.String({
|
|
52
|
+
description: "Document title",
|
|
53
|
+
}),
|
|
54
|
+
content: Type.String({
|
|
55
|
+
description: "Markdown content to import. Supports full Markdown syntax including tables, lists, code blocks, etc.",
|
|
56
|
+
}),
|
|
57
|
+
folder_token: Type.Optional(
|
|
58
|
+
Type.String({
|
|
59
|
+
description: "Target folder token (optional, defaults to root). Use 'list' to find folder tokens.",
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
doc_type: Type.Optional(DocType),
|
|
63
|
+
}),
|
|
44
64
|
]);
|
|
45
65
|
|
|
46
66
|
export type FeishuDriveParams = Static<typeof FeishuDriveSchema>;
|
package/src/drive.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { createFeishuClient } from "./client.js";
|
|
3
|
-
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
2
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
5
3
|
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
|
6
|
-
import {
|
|
4
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "./tools-common/tool-exec.js";
|
|
5
|
+
import { createAndWriteDoc } from "./doc-write-service.js";
|
|
7
6
|
|
|
8
7
|
// ============ Helpers ============
|
|
9
8
|
|
|
@@ -147,6 +146,24 @@ async function deleteFile(client: Lark.Client, fileToken: string, type: string)
|
|
|
147
146
|
};
|
|
148
147
|
}
|
|
149
148
|
|
|
149
|
+
// ============ Import Document Functions ============
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Import markdown content as a new Feishu document
|
|
153
|
+
* Uses create + write approach for reliable content import.
|
|
154
|
+
* Note: docType parameter is accepted for API compatibility but docx is always used.
|
|
155
|
+
*/
|
|
156
|
+
async function importDocument(
|
|
157
|
+
client: Lark.Client,
|
|
158
|
+
title: string,
|
|
159
|
+
content: string,
|
|
160
|
+
mediaMaxBytes: number,
|
|
161
|
+
folderToken?: string,
|
|
162
|
+
_docType?: "docx" | "doc",
|
|
163
|
+
) {
|
|
164
|
+
return createAndWriteDoc(client, title, content, mediaMaxBytes, folderToken);
|
|
165
|
+
}
|
|
166
|
+
|
|
150
167
|
// ============ Tool Registration ============
|
|
151
168
|
|
|
152
169
|
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
@@ -155,46 +172,59 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
|
155
172
|
return;
|
|
156
173
|
}
|
|
157
174
|
|
|
158
|
-
|
|
159
|
-
if (accounts.length === 0) {
|
|
175
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
160
176
|
api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools");
|
|
161
177
|
return;
|
|
162
178
|
}
|
|
163
179
|
|
|
164
|
-
|
|
165
|
-
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
|
166
|
-
if (!toolsCfg.drive) {
|
|
180
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "drive")) {
|
|
167
181
|
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
|
168
182
|
return;
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
const getClient = () => createFeishuClient(firstAccount);
|
|
172
|
-
|
|
173
185
|
api.registerTool(
|
|
174
186
|
{
|
|
175
187
|
name: "feishu_drive",
|
|
176
188
|
label: "Feishu Drive",
|
|
177
189
|
description:
|
|
178
|
-
"Feishu cloud storage operations. Actions: list, info, create_folder, move, delete",
|
|
190
|
+
"Feishu cloud storage operations. Actions: list, info, create_folder, move, delete, import_document. Use 'import_document' to create documents from Markdown with better structure preservation than block-by-block writing.",
|
|
179
191
|
parameters: FeishuDriveSchema,
|
|
180
192
|
async execute(_toolCallId, params) {
|
|
181
193
|
const p = params as FeishuDriveParams;
|
|
182
194
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
return await withFeishuToolClient({
|
|
196
|
+
api,
|
|
197
|
+
toolName: "feishu_drive",
|
|
198
|
+
requiredTool: "drive",
|
|
199
|
+
run: async ({ client, account }) => {
|
|
200
|
+
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
201
|
+
switch (p.action) {
|
|
202
|
+
case "list":
|
|
203
|
+
return json(await listFolder(client, p.folder_token));
|
|
204
|
+
case "info":
|
|
205
|
+
return json(await getFileInfo(client, p.file_token));
|
|
206
|
+
case "create_folder":
|
|
207
|
+
return json(await createFolder(client, p.name, p.folder_token));
|
|
208
|
+
case "move":
|
|
209
|
+
return json(await moveFile(client, p.file_token, p.type, p.folder_token));
|
|
210
|
+
case "delete":
|
|
211
|
+
return json(await deleteFile(client, p.file_token, p.type));
|
|
212
|
+
case "import_document":
|
|
213
|
+
return json(
|
|
214
|
+
await importDocument(
|
|
215
|
+
client,
|
|
216
|
+
p.title,
|
|
217
|
+
p.content,
|
|
218
|
+
mediaMaxBytes,
|
|
219
|
+
p.folder_token,
|
|
220
|
+
(p as any).doc_type || "docx",
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
default:
|
|
224
|
+
return json({ error: `Unknown action: ${(p as any).action}` });
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
});
|
|
198
228
|
} catch (err) {
|
|
199
229
|
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
200
230
|
}
|
|
@@ -203,5 +233,5 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
|
203
233
|
{ name: "feishu_drive" },
|
|
204
234
|
);
|
|
205
235
|
|
|
206
|
-
api.logger.
|
|
236
|
+
api.logger.debug?.("feishu_drive: Registered feishu_drive tool");
|
|
207
237
|
}
|
package/src/dynamic-agent.ts
CHANGED
|
@@ -19,16 +19,18 @@ export async function maybeCreateDynamicAgent(params: {
|
|
|
19
19
|
runtime: PluginRuntime;
|
|
20
20
|
senderOpenId: string;
|
|
21
21
|
dynamicCfg: DynamicAgentCreationConfig;
|
|
22
|
+
accountId?: string;
|
|
22
23
|
log: (msg: string) => void;
|
|
23
24
|
}): Promise<MaybeCreateDynamicAgentResult> {
|
|
24
|
-
const { cfg, runtime, senderOpenId, dynamicCfg, log } = params;
|
|
25
|
+
const { cfg, runtime, senderOpenId, dynamicCfg, accountId, log } = params;
|
|
25
26
|
|
|
26
27
|
// Check if there's already a binding for this user
|
|
27
28
|
const existingBindings = cfg.bindings ?? [];
|
|
28
29
|
const hasBinding = existingBindings.some(
|
|
29
30
|
(b) =>
|
|
30
31
|
b.match?.channel === "feishu" &&
|
|
31
|
-
b.match?.
|
|
32
|
+
(!accountId || b.match?.accountId === accountId) &&
|
|
33
|
+
b.match?.peer?.kind === "direct" &&
|
|
32
34
|
b.match?.peer?.id === senderOpenId,
|
|
33
35
|
);
|
|
34
36
|
|
|
@@ -66,7 +68,8 @@ export async function maybeCreateDynamicAgent(params: {
|
|
|
66
68
|
agentId,
|
|
67
69
|
match: {
|
|
68
70
|
channel: "feishu",
|
|
69
|
-
|
|
71
|
+
...(accountId ? { accountId } : {}),
|
|
72
|
+
peer: { kind: "direct", id: senderOpenId },
|
|
70
73
|
},
|
|
71
74
|
},
|
|
72
75
|
],
|
|
@@ -108,7 +111,8 @@ export async function maybeCreateDynamicAgent(params: {
|
|
|
108
111
|
agentId,
|
|
109
112
|
match: {
|
|
110
113
|
channel: "feishu",
|
|
111
|
-
|
|
114
|
+
...(accountId ? { accountId } : {}),
|
|
115
|
+
peer: { kind: "direct", id: senderOpenId },
|
|
112
116
|
},
|
|
113
117
|
},
|
|
114
118
|
],
|