@m1heng-clawd/feishu 0.1.10 → 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.
- package/README.md +280 -7
- package/index.ts +6 -6
- package/package.json +13 -3
- package/skills/feishu-doc/SKILL.md +64 -2
- package/skills/feishu-task/SKILL.md +210 -0
- package/src/bitable-tools/index.ts +1 -0
- package/src/bot.ts +182 -71
- package/src/channel.ts +2 -0
- package/src/config-schema.ts +4 -0
- package/src/doc-tools/actions.ts +341 -0
- package/src/doc-tools/common.ts +33 -0
- package/src/doc-tools/index.ts +2 -0
- package/src/doc-tools/register.ts +90 -0
- package/src/{doc-schema.ts → doc-tools/schemas.ts} +39 -1
- package/src/{docx.ts → doc-write-service.ts} +204 -351
- package/src/drive-tools/actions.ts +182 -0
- package/src/drive-tools/common.ts +18 -0
- package/src/drive-tools/index.ts +2 -0
- package/src/drive-tools/register.ts +71 -0
- package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
- package/src/media.ts +5 -3
- package/src/onboarding.ts +14 -4
- package/src/perm-tools/actions.ts +111 -0
- package/src/perm-tools/common.ts +18 -0
- package/src/perm-tools/index.ts +2 -0
- package/src/perm-tools/register.ts +65 -0
- package/src/policy.ts +13 -0
- package/src/reply-dispatcher.ts +6 -4
- package/src/send.ts +33 -2
- package/src/task-tools/actions.ts +466 -13
- package/src/task-tools/constants.ts +13 -0
- package/src/task-tools/index.ts +1 -0
- package/src/task-tools/register.ts +175 -13
- package/src/task-tools/schemas.ts +433 -4
- package/src/text/markdown-links.ts +104 -0
- package/src/types.ts +1 -0
- package/src/wiki-tools/actions.ts +166 -0
- package/src/wiki-tools/common.ts +18 -0
- package/src/wiki-tools/index.ts +2 -0
- package/src/wiki-tools/register.ts +66 -0
- package/src/bitable.ts +0 -1
- package/src/drive.ts +0 -264
- package/src/perm.ts +0 -165
- package/src/task.ts +0 -1
- package/src/wiki.ts +0 -223
- /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
- /package/src/{wiki-schema.ts → wiki-tools/schemas.ts} +0 -0
|
@@ -1,33 +1,6 @@
|
|
|
1
|
-
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
1
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
4
2
|
import { Readable } from "stream";
|
|
5
|
-
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
|
6
3
|
import { getFeishuRuntime } from "./runtime.js";
|
|
7
|
-
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "./tools-common/tool-exec.js";
|
|
8
|
-
|
|
9
|
-
// ============ Helpers ============
|
|
10
|
-
|
|
11
|
-
function json(data: unknown) {
|
|
12
|
-
return {
|
|
13
|
-
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
14
|
-
details: data,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Extract image URLs from markdown content */
|
|
19
|
-
function extractImageUrls(markdown: string): string[] {
|
|
20
|
-
const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
21
|
-
const urls: string[] = [];
|
|
22
|
-
let match;
|
|
23
|
-
while ((match = regex.exec(markdown)) !== null) {
|
|
24
|
-
const url = match[1].trim();
|
|
25
|
-
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
26
|
-
urls.push(url);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return urls;
|
|
30
|
-
}
|
|
31
4
|
|
|
32
5
|
const BLOCK_TYPE_NAMES: Record<number, string> = {
|
|
33
6
|
1: "Page",
|
|
@@ -53,6 +26,59 @@ const BLOCK_TYPE_NAMES: Record<number, string> = {
|
|
|
53
26
|
// Block types that cannot be created via documentBlockChildren.create API
|
|
54
27
|
const UNSUPPORTED_CREATE_TYPES = new Set([32]);
|
|
55
28
|
|
|
29
|
+
// Maximum content length for a single API call (empirical value based on Feishu API limits)
|
|
30
|
+
const MAX_CONTENT_LENGTH = 50000; // ~50KB
|
|
31
|
+
const MAX_BLOCKS_PER_INSERT = 50; // Maximum blocks per insert API call
|
|
32
|
+
|
|
33
|
+
export type CreateDocResult = {
|
|
34
|
+
document_id?: string;
|
|
35
|
+
title?: string;
|
|
36
|
+
url: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type WriteDocResult = {
|
|
40
|
+
success: true;
|
|
41
|
+
blocks_deleted: number;
|
|
42
|
+
blocks_added: number;
|
|
43
|
+
images_processed: number;
|
|
44
|
+
warning?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type AppendDocResult = {
|
|
48
|
+
success: true;
|
|
49
|
+
blocks_added: number;
|
|
50
|
+
images_processed: number;
|
|
51
|
+
block_ids: string[];
|
|
52
|
+
warning?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type CreateAndWriteDocResult = {
|
|
56
|
+
success: true;
|
|
57
|
+
document_id: string;
|
|
58
|
+
title: string;
|
|
59
|
+
url: string;
|
|
60
|
+
import_method: "create_and_write";
|
|
61
|
+
blocks_added: number;
|
|
62
|
+
images_processed: number;
|
|
63
|
+
warning?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type InsertResult = { children: any[]; skipped: string[]; warnings: string[] };
|
|
67
|
+
|
|
68
|
+
/** Extract image URLs from markdown content */
|
|
69
|
+
function extractImageUrls(markdown: string): string[] {
|
|
70
|
+
const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
71
|
+
const urls: string[] = [];
|
|
72
|
+
let match;
|
|
73
|
+
while ((match = regex.exec(markdown)) !== null) {
|
|
74
|
+
const url = match[1].trim();
|
|
75
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
76
|
+
urls.push(url);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return urls;
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
/**
|
|
57
83
|
* Reorder blocks according to firstLevelBlockIds from convertMarkdown API.
|
|
58
84
|
* The API returns blocks as a flat unordered array across all levels.
|
|
@@ -60,14 +86,14 @@ const UNSUPPORTED_CREATE_TYPES = new Set([32]);
|
|
|
60
86
|
*/
|
|
61
87
|
function reorderBlocks(blocks: any[], firstLevelBlockIds: string[]): any[] {
|
|
62
88
|
if (!firstLevelBlockIds || firstLevelBlockIds.length === 0) return blocks;
|
|
63
|
-
|
|
89
|
+
|
|
64
90
|
const blockMap = new Map<string, any>();
|
|
65
91
|
for (const block of blocks) {
|
|
66
92
|
if (block.block_id) {
|
|
67
93
|
blockMap.set(block.block_id, block);
|
|
68
94
|
}
|
|
69
95
|
}
|
|
70
|
-
|
|
96
|
+
|
|
71
97
|
const ordered: any[] = [];
|
|
72
98
|
for (const id of firstLevelBlockIds) {
|
|
73
99
|
const block = blockMap.get(id);
|
|
@@ -118,8 +144,6 @@ function buildBlockMap(blocks: any[]): Map<string, any> {
|
|
|
118
144
|
return map;
|
|
119
145
|
}
|
|
120
146
|
|
|
121
|
-
type InsertResult = { children: any[]; skipped: string[]; warnings: string[] };
|
|
122
|
-
|
|
123
147
|
function sleep(ms: number) {
|
|
124
148
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
125
149
|
}
|
|
@@ -214,6 +238,27 @@ async function createChildrenWithRetry(
|
|
|
214
238
|
});
|
|
215
239
|
}
|
|
216
240
|
|
|
241
|
+
async function insertBlocks(
|
|
242
|
+
client: Lark.Client,
|
|
243
|
+
docToken: string,
|
|
244
|
+
blocks: any[],
|
|
245
|
+
parentBlockId?: string,
|
|
246
|
+
): Promise<{ children: any[]; skipped: string[] }> {
|
|
247
|
+
const { cleaned, skipped } = cleanBlocksForInsert(blocks);
|
|
248
|
+
const blockId = parentBlockId ?? docToken;
|
|
249
|
+
|
|
250
|
+
if (cleaned.length === 0) {
|
|
251
|
+
return { children: [], skipped };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const res = await createChildrenWithRetry(client, {
|
|
255
|
+
path: { document_id: docToken, block_id: blockId },
|
|
256
|
+
data: { children: cleaned },
|
|
257
|
+
});
|
|
258
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
259
|
+
return { children: res.data?.children ?? [], skipped };
|
|
260
|
+
}
|
|
261
|
+
|
|
217
262
|
async function insertTableWithCells(
|
|
218
263
|
client: Lark.Client,
|
|
219
264
|
docToken: string,
|
|
@@ -258,9 +303,7 @@ async function insertTableWithCells(
|
|
|
258
303
|
const dstCellId = dstCells[i];
|
|
259
304
|
const srcCell = blockMap.get(srcCellId);
|
|
260
305
|
const srcChildIds: string[] = srcCell?.children ?? [];
|
|
261
|
-
let srcChildBlocks = srcChildIds
|
|
262
|
-
.map((id) => blockMap.get(id))
|
|
263
|
-
.filter((b): b is any => Boolean(b));
|
|
306
|
+
let srcChildBlocks = srcChildIds.map((id) => blockMap.get(id)).filter((b): b is any => Boolean(b));
|
|
264
307
|
|
|
265
308
|
// Some convert payloads may carry plain text directly on table_cell.
|
|
266
309
|
if (srcChildBlocks.length === 0 && srcCell?.text?.elements?.length) {
|
|
@@ -297,6 +340,64 @@ async function insertTableWithCells(
|
|
|
297
340
|
};
|
|
298
341
|
}
|
|
299
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Insert blocks in batches to avoid API limits.
|
|
345
|
+
*/
|
|
346
|
+
async function insertBlocksInBatches(
|
|
347
|
+
client: Lark.Client,
|
|
348
|
+
docToken: string,
|
|
349
|
+
blocks: any[],
|
|
350
|
+
parentBlockId?: string,
|
|
351
|
+
): Promise<{ children: any[]; skipped: string[] }> {
|
|
352
|
+
const allInserted: any[] = [];
|
|
353
|
+
const allSkipped: string[] = [];
|
|
354
|
+
const blockId = parentBlockId ?? docToken;
|
|
355
|
+
|
|
356
|
+
for (let i = 0; i < blocks.length; i += MAX_BLOCKS_PER_INSERT) {
|
|
357
|
+
const batch = blocks.slice(i, i + MAX_BLOCKS_PER_INSERT);
|
|
358
|
+
const { cleaned, skipped } = cleanBlocksForInsert(batch);
|
|
359
|
+
allSkipped.push(...skipped);
|
|
360
|
+
|
|
361
|
+
if (cleaned.length === 0) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const res = await createChildrenWithRetry(client, {
|
|
367
|
+
path: { document_id: docToken, block_id: blockId },
|
|
368
|
+
data: { children: cleaned },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
if (res.code !== 0) {
|
|
372
|
+
// If batch insert fails, try inserting one by one.
|
|
373
|
+
console.warn(`[feishu_doc] Batch insert failed: ${res.msg}. Trying individual inserts...`);
|
|
374
|
+
for (const block of cleaned) {
|
|
375
|
+
try {
|
|
376
|
+
const singleRes = await createChildrenWithRetry(client, {
|
|
377
|
+
path: { document_id: docToken, block_id: blockId },
|
|
378
|
+
data: { children: [block] },
|
|
379
|
+
});
|
|
380
|
+
if (singleRes.code === 0) {
|
|
381
|
+
allInserted.push(...(singleRes.data?.children ?? []));
|
|
382
|
+
} else {
|
|
383
|
+
console.error(`[feishu_doc] Failed to insert block: ${singleRes.msg}`);
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
console.error(`[feishu_doc] Error inserting block:`, err);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
allInserted.push(...(res.data?.children ?? []));
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.error(`[feishu_doc] Error in batch insert:`, err);
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { children: allInserted, skipped: [...new Set(allSkipped)] };
|
|
399
|
+
}
|
|
400
|
+
|
|
300
401
|
async function insertBlocksPreservingTables(
|
|
301
402
|
client: Lark.Client,
|
|
302
403
|
docToken: string,
|
|
@@ -338,8 +439,6 @@ async function insertBlocksPreservingTables(
|
|
|
338
439
|
};
|
|
339
440
|
}
|
|
340
441
|
|
|
341
|
-
// ============ Core Functions ============
|
|
342
|
-
|
|
343
442
|
async function convertMarkdown(client: Lark.Client, markdown: string) {
|
|
344
443
|
const res = await client.docx.document.convert({
|
|
345
444
|
data: { content_type: "markdown", content: markdown },
|
|
@@ -351,27 +450,6 @@ async function convertMarkdown(client: Lark.Client, markdown: string) {
|
|
|
351
450
|
};
|
|
352
451
|
}
|
|
353
452
|
|
|
354
|
-
async function insertBlocks(
|
|
355
|
-
client: Lark.Client,
|
|
356
|
-
docToken: string,
|
|
357
|
-
blocks: any[],
|
|
358
|
-
parentBlockId?: string,
|
|
359
|
-
): Promise<{ children: any[]; skipped: string[] }> {
|
|
360
|
-
const { cleaned, skipped } = cleanBlocksForInsert(blocks);
|
|
361
|
-
const blockId = parentBlockId ?? docToken;
|
|
362
|
-
|
|
363
|
-
if (cleaned.length === 0) {
|
|
364
|
-
return { children: [], skipped };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const res = await createChildrenWithRetry(client, {
|
|
368
|
-
path: { document_id: docToken, block_id: blockId },
|
|
369
|
-
data: { children: cleaned },
|
|
370
|
-
});
|
|
371
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
372
|
-
return { children: res.data?.children ?? [], skipped };
|
|
373
|
-
}
|
|
374
|
-
|
|
375
453
|
async function clearDocumentContent(client: Lark.Client, docToken: string) {
|
|
376
454
|
const existing = await client.docx.documentBlock.list({
|
|
377
455
|
path: { document_id: docToken },
|
|
@@ -433,8 +511,8 @@ async function processImages(
|
|
|
433
511
|
if (imageUrls.length === 0) return 0;
|
|
434
512
|
|
|
435
513
|
const imageBlocks = insertedBlocks.filter((b) => b.block_type === 27);
|
|
436
|
-
|
|
437
514
|
let processed = 0;
|
|
515
|
+
|
|
438
516
|
for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
|
|
439
517
|
const url = imageUrls[i];
|
|
440
518
|
const blockId = imageBlocks[i].block_id;
|
|
@@ -461,49 +539,34 @@ async function processImages(
|
|
|
461
539
|
return processed;
|
|
462
540
|
}
|
|
463
541
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
]);
|
|
474
|
-
|
|
475
|
-
if (contentRes.code !== 0) throw new Error(contentRes.msg);
|
|
476
|
-
|
|
477
|
-
const blocks = blocksRes.data?.items ?? [];
|
|
478
|
-
const blockCounts: Record<string, number> = {};
|
|
479
|
-
const structuredTypes: string[] = [];
|
|
480
|
-
|
|
481
|
-
for (const b of blocks) {
|
|
482
|
-
const type = b.block_type ?? 0;
|
|
483
|
-
const name = BLOCK_TYPE_NAMES[type] || `type_${type}`;
|
|
484
|
-
blockCounts[name] = (blockCounts[name] || 0) + 1;
|
|
485
|
-
|
|
486
|
-
if (STRUCTURED_BLOCK_TYPES.has(type) && !structuredTypes.includes(name)) {
|
|
487
|
-
structuredTypes.push(name);
|
|
488
|
-
}
|
|
542
|
+
function ensureBlocksInserted(args: {
|
|
543
|
+
mode: "write" | "append";
|
|
544
|
+
markdown: string;
|
|
545
|
+
insertedCount: number;
|
|
546
|
+
skipped: string[];
|
|
547
|
+
warnings: string[];
|
|
548
|
+
}) {
|
|
549
|
+
if (args.markdown.trim().length === 0) {
|
|
550
|
+
return;
|
|
489
551
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (structuredTypes.length > 0) {
|
|
493
|
-
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.`;
|
|
552
|
+
if (args.insertedCount > 0) {
|
|
553
|
+
return;
|
|
494
554
|
}
|
|
495
555
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
};
|
|
556
|
+
const details: string[] = [];
|
|
557
|
+
if (args.skipped.length > 0) details.push(`skipped=${args.skipped.join(", ")}`);
|
|
558
|
+
if (args.warnings.length > 0) details.push(`warnings=${args.warnings.join(" | ")}`);
|
|
559
|
+
const suffix = details.length > 0 ? ` (${details.join("; ")})` : "";
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Document ${args.mode} produced zero inserted blocks for non-empty content${suffix}. Check markdown compatibility and granted scopes.`,
|
|
562
|
+
);
|
|
504
563
|
}
|
|
505
564
|
|
|
506
|
-
async function createDoc(
|
|
565
|
+
export async function createDoc(
|
|
566
|
+
client: Lark.Client,
|
|
567
|
+
title: string,
|
|
568
|
+
folderToken?: string,
|
|
569
|
+
): Promise<CreateDocResult> {
|
|
507
570
|
const res = await client.docx.document.create({
|
|
508
571
|
data: { title, folder_token: folderToken },
|
|
509
572
|
});
|
|
@@ -516,35 +579,30 @@ async function createDoc(client: Lark.Client, title: string, folderToken?: strin
|
|
|
516
579
|
};
|
|
517
580
|
}
|
|
518
581
|
|
|
519
|
-
// Maximum content length for a single API call (empirical value based on Feishu API limits)
|
|
520
|
-
const MAX_CONTENT_LENGTH = 50000; // ~50KB
|
|
521
|
-
const MAX_BLOCKS_PER_INSERT = 50; // Maximum blocks per insert API call
|
|
522
|
-
|
|
523
582
|
export async function writeDoc(
|
|
524
583
|
client: Lark.Client,
|
|
525
584
|
docToken: string,
|
|
526
585
|
markdown: string,
|
|
527
586
|
maxBytes: number,
|
|
528
|
-
) {
|
|
587
|
+
): Promise<WriteDocResult> {
|
|
529
588
|
const deleted = await clearDocumentContent(client, docToken);
|
|
530
589
|
|
|
531
|
-
// Check content length and warn if too long
|
|
532
590
|
if (markdown.length > MAX_CONTENT_LENGTH) {
|
|
533
|
-
console.warn(
|
|
591
|
+
console.warn(
|
|
592
|
+
`[feishu_doc] Content length (${markdown.length}) exceeds recommended limit (${MAX_CONTENT_LENGTH}). May cause API errors.`,
|
|
593
|
+
);
|
|
534
594
|
}
|
|
535
595
|
|
|
536
596
|
const { blocks, firstLevelBlockIds } = await convertMarkdown(client, markdown);
|
|
537
597
|
if (blocks.length === 0) {
|
|
598
|
+
if (markdown.trim().length > 0) {
|
|
599
|
+
throw new Error("Markdown conversion returned no blocks for non-empty content.");
|
|
600
|
+
}
|
|
538
601
|
return { success: true, blocks_deleted: deleted, blocks_added: 0, images_processed: 0 };
|
|
539
602
|
}
|
|
540
603
|
|
|
541
|
-
// Reorder blocks according to firstLevelBlockIds to maintain correct document order.
|
|
542
|
-
// The convertMarkdown API returns blocks in an unordered map; firstLevelBlockIds
|
|
543
|
-
// provides the correct top-level ordering.
|
|
544
604
|
const orderedBlocks = reorderBlocks(blocks, firstLevelBlockIds);
|
|
545
605
|
const blockMap = buildBlockMap(blocks);
|
|
546
|
-
|
|
547
|
-
// Insert blocks while preserving table content when possible.
|
|
548
606
|
const { children: inserted, skipped, warnings } = await insertBlocksPreservingTables(
|
|
549
607
|
client,
|
|
550
608
|
docToken,
|
|
@@ -552,6 +610,13 @@ export async function writeDoc(
|
|
|
552
610
|
blockMap,
|
|
553
611
|
);
|
|
554
612
|
const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
|
|
613
|
+
ensureBlocksInserted({
|
|
614
|
+
mode: "write",
|
|
615
|
+
markdown,
|
|
616
|
+
insertedCount: inserted.length,
|
|
617
|
+
skipped,
|
|
618
|
+
warnings,
|
|
619
|
+
});
|
|
555
620
|
|
|
556
621
|
const warningParts: string[] = [];
|
|
557
622
|
if (skipped.length > 0) {
|
|
@@ -572,75 +637,18 @@ export async function writeDoc(
|
|
|
572
637
|
};
|
|
573
638
|
}
|
|
574
639
|
|
|
575
|
-
|
|
576
|
-
* Insert blocks in batches to avoid API limits
|
|
577
|
-
*/
|
|
578
|
-
async function insertBlocksInBatches(
|
|
640
|
+
export async function appendDoc(
|
|
579
641
|
client: Lark.Client,
|
|
580
642
|
docToken: string,
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
): Promise<
|
|
584
|
-
const allInserted: any[] = [];
|
|
585
|
-
const allSkipped: string[] = [];
|
|
586
|
-
const blockId = parentBlockId ?? docToken;
|
|
587
|
-
|
|
588
|
-
// Process blocks in batches
|
|
589
|
-
for (let i = 0; i < blocks.length; i += MAX_BLOCKS_PER_INSERT) {
|
|
590
|
-
const batch = blocks.slice(i, i + MAX_BLOCKS_PER_INSERT);
|
|
591
|
-
const { cleaned, skipped } = cleanBlocksForInsert(batch);
|
|
592
|
-
|
|
593
|
-
allSkipped.push(...skipped);
|
|
594
|
-
|
|
595
|
-
if (cleaned.length === 0) {
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
try {
|
|
600
|
-
const res = await createChildrenWithRetry(client, {
|
|
601
|
-
path: { document_id: docToken, block_id: blockId },
|
|
602
|
-
data: { children: cleaned },
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
if (res.code !== 0) {
|
|
606
|
-
// If batch insert fails, try inserting one by one
|
|
607
|
-
console.warn(`[feishu_doc] Batch insert failed: ${res.msg}. Trying individual inserts...`);
|
|
608
|
-
for (const block of cleaned) {
|
|
609
|
-
try {
|
|
610
|
-
const singleRes = await createChildrenWithRetry(client, {
|
|
611
|
-
path: { document_id: docToken, block_id: blockId },
|
|
612
|
-
data: { children: [block] },
|
|
613
|
-
});
|
|
614
|
-
if (singleRes.code === 0) {
|
|
615
|
-
allInserted.push(...(singleRes.data?.children ?? []));
|
|
616
|
-
} else {
|
|
617
|
-
console.error(`[feishu_doc] Failed to insert block: ${singleRes.msg}`);
|
|
618
|
-
}
|
|
619
|
-
} catch (err) {
|
|
620
|
-
console.error(`[feishu_doc] Error inserting block:`, err);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
} else {
|
|
624
|
-
allInserted.push(...(res.data?.children ?? []));
|
|
625
|
-
}
|
|
626
|
-
} catch (err) {
|
|
627
|
-
console.error(`[feishu_doc] Error in batch insert:`, err);
|
|
628
|
-
throw err;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
return { children: allInserted, skipped: [...new Set(allSkipped)] };
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
async function appendDoc(client: Lark.Client, docToken: string, markdown: string, maxBytes: number) {
|
|
643
|
+
markdown: string,
|
|
644
|
+
maxBytes: number,
|
|
645
|
+
): Promise<AppendDocResult> {
|
|
636
646
|
const { blocks, firstLevelBlockIds } = await convertMarkdown(client, markdown);
|
|
637
647
|
if (blocks.length === 0) {
|
|
638
648
|
throw new Error("Content is empty");
|
|
639
649
|
}
|
|
640
650
|
|
|
641
|
-
// Reorder blocks according to firstLevelBlockIds (same fix as writeDoc)
|
|
642
651
|
const orderedBlocks = reorderBlocks(blocks, firstLevelBlockIds);
|
|
643
|
-
|
|
644
652
|
const blockMap = buildBlockMap(blocks);
|
|
645
653
|
const { children: inserted, skipped, warnings } = await insertBlocksPreservingTables(
|
|
646
654
|
client,
|
|
@@ -649,6 +657,13 @@ async function appendDoc(client: Lark.Client, docToken: string, markdown: string
|
|
|
649
657
|
blockMap,
|
|
650
658
|
);
|
|
651
659
|
const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
|
|
660
|
+
ensureBlocksInserted({
|
|
661
|
+
mode: "append",
|
|
662
|
+
markdown,
|
|
663
|
+
insertedCount: inserted.length,
|
|
664
|
+
skipped,
|
|
665
|
+
warnings,
|
|
666
|
+
});
|
|
652
667
|
|
|
653
668
|
const warningParts: string[] = [];
|
|
654
669
|
if (skipped.length > 0) {
|
|
@@ -669,190 +684,28 @@ async function appendDoc(client: Lark.Client, docToken: string, markdown: string
|
|
|
669
684
|
};
|
|
670
685
|
}
|
|
671
686
|
|
|
672
|
-
async function
|
|
687
|
+
export async function createAndWriteDoc(
|
|
673
688
|
client: Lark.Client,
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
path: { document_id: docToken, block_id: blockId },
|
|
685
|
-
data: {
|
|
686
|
-
update_text_elements: {
|
|
687
|
-
elements: [{ text_run: { content } }],
|
|
688
|
-
},
|
|
689
|
-
},
|
|
690
|
-
});
|
|
691
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
692
|
-
|
|
693
|
-
return { success: true, block_id: blockId };
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
async function deleteBlock(client: Lark.Client, docToken: string, blockId: string) {
|
|
697
|
-
const blockInfo = await client.docx.documentBlock.get({
|
|
698
|
-
path: { document_id: docToken, block_id: blockId },
|
|
699
|
-
});
|
|
700
|
-
if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
|
|
701
|
-
|
|
702
|
-
const parentId = blockInfo.data?.block?.parent_id ?? docToken;
|
|
703
|
-
|
|
704
|
-
const children = await client.docx.documentBlockChildren.get({
|
|
705
|
-
path: { document_id: docToken, block_id: parentId },
|
|
706
|
-
});
|
|
707
|
-
if (children.code !== 0) throw new Error(children.msg);
|
|
708
|
-
|
|
709
|
-
const items = children.data?.items ?? [];
|
|
710
|
-
const index = items.findIndex((item: any) => item.block_id === blockId);
|
|
711
|
-
if (index === -1) throw new Error("Block not found");
|
|
712
|
-
|
|
713
|
-
const res = await client.docx.documentBlockChildren.batchDelete({
|
|
714
|
-
path: { document_id: docToken, block_id: parentId },
|
|
715
|
-
data: { start_index: index, end_index: index + 1 },
|
|
716
|
-
});
|
|
717
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
718
|
-
|
|
719
|
-
return { success: true, deleted_block_id: blockId };
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async function listBlocks(client: Lark.Client, docToken: string) {
|
|
723
|
-
const res = await client.docx.documentBlock.list({
|
|
724
|
-
path: { document_id: docToken },
|
|
725
|
-
});
|
|
726
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
727
|
-
|
|
728
|
-
return {
|
|
729
|
-
blocks: res.data?.items ?? [],
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
async function getBlock(client: Lark.Client, docToken: string, blockId: string) {
|
|
734
|
-
const res = await client.docx.documentBlock.get({
|
|
735
|
-
path: { document_id: docToken, block_id: blockId },
|
|
736
|
-
});
|
|
737
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
738
|
-
|
|
739
|
-
return {
|
|
740
|
-
block: res.data?.block,
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
async function listAppScopes(client: Lark.Client) {
|
|
745
|
-
const res = await client.application.scope.list({});
|
|
746
|
-
if (res.code !== 0) throw new Error(res.msg);
|
|
747
|
-
|
|
748
|
-
const scopes = res.data?.scopes ?? [];
|
|
749
|
-
const granted = scopes.filter((s) => s.grant_status === 1);
|
|
750
|
-
const pending = scopes.filter((s) => s.grant_status !== 1);
|
|
689
|
+
title: string,
|
|
690
|
+
markdown: string,
|
|
691
|
+
maxBytes: number,
|
|
692
|
+
folderToken?: string,
|
|
693
|
+
): Promise<CreateAndWriteDocResult> {
|
|
694
|
+
const created = await createDoc(client, title, folderToken);
|
|
695
|
+
const docId = created.document_id;
|
|
696
|
+
if (!docId) {
|
|
697
|
+
throw new Error("Document created but no document_id returned");
|
|
698
|
+
}
|
|
751
699
|
|
|
700
|
+
const writeResult = await writeDoc(client, docId, markdown, maxBytes);
|
|
752
701
|
return {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
702
|
+
success: true,
|
|
703
|
+
document_id: docId,
|
|
704
|
+
title: created.title ?? title,
|
|
705
|
+
url: created.url,
|
|
706
|
+
import_method: "create_and_write",
|
|
707
|
+
blocks_added: writeResult.blocks_added,
|
|
708
|
+
images_processed: writeResult.images_processed,
|
|
709
|
+
...(writeResult.warning && { warning: writeResult.warning }),
|
|
756
710
|
};
|
|
757
711
|
}
|
|
758
|
-
|
|
759
|
-
// ============ Tool Registration ============
|
|
760
|
-
|
|
761
|
-
export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
|
762
|
-
if (!api.config) {
|
|
763
|
-
api.logger.debug?.("feishu_doc: No config available, skipping doc tools");
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
768
|
-
api.logger.debug?.("feishu_doc: No Feishu accounts configured, skipping doc tools");
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// Registration happens once; account selection happens per execute() call.
|
|
773
|
-
const docEnabled = hasFeishuToolEnabledForAnyAccount(api.config, "doc");
|
|
774
|
-
const scopesEnabled = hasFeishuToolEnabledForAnyAccount(api.config, "scopes");
|
|
775
|
-
const registered: string[] = [];
|
|
776
|
-
|
|
777
|
-
// Main document tool with action-based dispatch
|
|
778
|
-
if (docEnabled) {
|
|
779
|
-
api.registerTool(
|
|
780
|
-
{
|
|
781
|
-
name: "feishu_doc",
|
|
782
|
-
label: "Feishu Doc",
|
|
783
|
-
description:
|
|
784
|
-
"Feishu document operations. Actions: read, write, append, create, list_blocks, get_block, update_block, delete_block",
|
|
785
|
-
parameters: FeishuDocSchema,
|
|
786
|
-
async execute(_toolCallId, params) {
|
|
787
|
-
const p = params as FeishuDocParams;
|
|
788
|
-
try {
|
|
789
|
-
return await withFeishuToolClient({
|
|
790
|
-
api,
|
|
791
|
-
toolName: "feishu_doc",
|
|
792
|
-
requiredTool: "doc",
|
|
793
|
-
run: async ({ client, account }) => {
|
|
794
|
-
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
795
|
-
switch (p.action) {
|
|
796
|
-
case "read":
|
|
797
|
-
return json(await readDoc(client, p.doc_token));
|
|
798
|
-
case "write":
|
|
799
|
-
return json(await writeDoc(client, p.doc_token, p.content, mediaMaxBytes));
|
|
800
|
-
case "append":
|
|
801
|
-
return json(await appendDoc(client, p.doc_token, p.content, mediaMaxBytes));
|
|
802
|
-
case "create":
|
|
803
|
-
return json(await createDoc(client, p.title, p.folder_token));
|
|
804
|
-
case "list_blocks":
|
|
805
|
-
return json(await listBlocks(client, p.doc_token));
|
|
806
|
-
case "get_block":
|
|
807
|
-
return json(await getBlock(client, p.doc_token, p.block_id));
|
|
808
|
-
case "update_block":
|
|
809
|
-
return json(await updateBlock(client, p.doc_token, p.block_id, p.content));
|
|
810
|
-
case "delete_block":
|
|
811
|
-
return json(await deleteBlock(client, p.doc_token, p.block_id));
|
|
812
|
-
default:
|
|
813
|
-
return json({ error: `Unknown action: ${(p as any).action}` });
|
|
814
|
-
}
|
|
815
|
-
},
|
|
816
|
-
});
|
|
817
|
-
} catch (err) {
|
|
818
|
-
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
819
|
-
}
|
|
820
|
-
},
|
|
821
|
-
},
|
|
822
|
-
{ name: "feishu_doc" },
|
|
823
|
-
);
|
|
824
|
-
registered.push("feishu_doc");
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Keep feishu_app_scopes as independent tool
|
|
828
|
-
if (scopesEnabled) {
|
|
829
|
-
api.registerTool(
|
|
830
|
-
{
|
|
831
|
-
name: "feishu_app_scopes",
|
|
832
|
-
label: "Feishu App Scopes",
|
|
833
|
-
description:
|
|
834
|
-
"List current app permissions (scopes). Use to debug permission issues or check available capabilities.",
|
|
835
|
-
parameters: Type.Object({}),
|
|
836
|
-
async execute() {
|
|
837
|
-
try {
|
|
838
|
-
const result = await withFeishuToolClient({
|
|
839
|
-
api,
|
|
840
|
-
toolName: "feishu_app_scopes",
|
|
841
|
-
requiredTool: "scopes",
|
|
842
|
-
run: async ({ client }) => listAppScopes(client),
|
|
843
|
-
});
|
|
844
|
-
return json(result);
|
|
845
|
-
} catch (err) {
|
|
846
|
-
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
847
|
-
}
|
|
848
|
-
},
|
|
849
|
-
},
|
|
850
|
-
{ name: "feishu_app_scopes" },
|
|
851
|
-
);
|
|
852
|
-
registered.push("feishu_app_scopes");
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (registered.length > 0) {
|
|
856
|
-
api.logger.debug?.(`feishu_doc: Registered ${registered.join(", ")}`);
|
|
857
|
-
}
|
|
858
|
-
}
|