@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.
Files changed (47) hide show
  1. package/README.md +280 -7
  2. package/index.ts +6 -6
  3. package/package.json +13 -3
  4. package/skills/feishu-doc/SKILL.md +64 -2
  5. package/skills/feishu-task/SKILL.md +210 -0
  6. package/src/bitable-tools/index.ts +1 -0
  7. package/src/bot.ts +182 -71
  8. package/src/channel.ts +2 -0
  9. package/src/config-schema.ts +4 -0
  10. package/src/doc-tools/actions.ts +341 -0
  11. package/src/doc-tools/common.ts +33 -0
  12. package/src/doc-tools/index.ts +2 -0
  13. package/src/doc-tools/register.ts +90 -0
  14. package/src/{doc-schema.ts → doc-tools/schemas.ts} +39 -1
  15. package/src/{docx.ts → doc-write-service.ts} +204 -351
  16. package/src/drive-tools/actions.ts +182 -0
  17. package/src/drive-tools/common.ts +18 -0
  18. package/src/drive-tools/index.ts +2 -0
  19. package/src/drive-tools/register.ts +71 -0
  20. package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
  21. package/src/media.ts +5 -3
  22. package/src/onboarding.ts +14 -4
  23. package/src/perm-tools/actions.ts +111 -0
  24. package/src/perm-tools/common.ts +18 -0
  25. package/src/perm-tools/index.ts +2 -0
  26. package/src/perm-tools/register.ts +65 -0
  27. package/src/policy.ts +13 -0
  28. package/src/reply-dispatcher.ts +6 -4
  29. package/src/send.ts +33 -2
  30. package/src/task-tools/actions.ts +466 -13
  31. package/src/task-tools/constants.ts +13 -0
  32. package/src/task-tools/index.ts +1 -0
  33. package/src/task-tools/register.ts +175 -13
  34. package/src/task-tools/schemas.ts +433 -4
  35. package/src/text/markdown-links.ts +104 -0
  36. package/src/types.ts +1 -0
  37. package/src/wiki-tools/actions.ts +166 -0
  38. package/src/wiki-tools/common.ts +18 -0
  39. package/src/wiki-tools/index.ts +2 -0
  40. package/src/wiki-tools/register.ts +66 -0
  41. package/src/bitable.ts +0 -1
  42. package/src/drive.ts +0 -264
  43. package/src/perm.ts +0 -165
  44. package/src/task.ts +0 -1
  45. package/src/wiki.ts +0 -223
  46. /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
  47. /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
- // ============ Actions ============
465
-
466
- const STRUCTURED_BLOCK_TYPES = new Set([14, 18, 21, 23, 27, 30, 31, 32]);
467
-
468
- async function readDoc(client: Lark.Client, docToken: string) {
469
- const [contentRes, infoRes, blocksRes] = await Promise.all([
470
- client.docx.document.rawContent({ path: { document_id: docToken } }),
471
- client.docx.document.get({ path: { document_id: docToken } }),
472
- client.docx.documentBlock.list({ path: { document_id: docToken } }),
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
- let hint: string | undefined;
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
- return {
497
- title: infoRes.data?.document?.title,
498
- content: contentRes.data?.content,
499
- revision_id: infoRes.data?.document?.revision_id,
500
- block_count: blocks.length,
501
- block_types: blockCounts,
502
- ...(hint && { hint }),
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(client: Lark.Client, title: string, folderToken?: string) {
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(`[feishu_doc] Content length (${markdown.length}) exceeds recommended limit (${MAX_CONTENT_LENGTH}). May cause API errors.`);
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
- blocks: any[],
582
- parentBlockId?: string,
583
- ): Promise<{ children: any[]; skipped: string[] }> {
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 updateBlock(
687
+ export async function createAndWriteDoc(
673
688
  client: Lark.Client,
674
- docToken: string,
675
- blockId: string,
676
- content: string,
677
- ) {
678
- const blockInfo = await client.docx.documentBlock.get({
679
- path: { document_id: docToken, block_id: blockId },
680
- });
681
- if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
682
-
683
- const res = await client.docx.documentBlock.patch({
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
- granted: granted.map((s) => ({ name: s.scope_name, type: s.scope_type })),
754
- pending: pending.map((s) => ({ name: s.scope_name, type: s.scope_type })),
755
- summary: `${granted.length} granted, ${pending.length} pending`,
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
- }