@openclaw/feishu 2026.7.2-beta.1 → 2026.7.2-beta.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/dist/api.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { r as listEnabledFeishuAccounts } from "./accounts-BDoGHJDk.js";
2
- import { a as setFeishuNamedAccountEnabled, i as feishuSetupAdapter, n as feishuSetupWizard, r as runFeishuLogin, t as feishuPlugin } from "./channel-DdPQ2HGh.js";
2
+ import { a as setFeishuNamedAccountEnabled, i as feishuSetupAdapter, n as feishuSetupWizard, r as runFeishuLogin, t as feishuPlugin } from "./channel-X30GQVwc.js";
3
+ import { p as parseFeishuMarkdown, u as chunkFeishuMarkdown } from "./send-result-DsqTk27v.js";
3
4
  import { a as parseFeishuTargetId, i as parseFeishuDirectConversationId, n as buildFeishuModelOverrideParentCandidates, r as parseFeishuConversationId, t as buildFeishuConversationId } from "./conversation-id-BeJL-wq7.js";
5
+ import { r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-87BmeMpj.js";
4
6
  import { t as getFeishuRuntime } from "./runtime-C5JxBWZp.js";
5
- import { r as createFeishuClient } from "./client-BUC-R2Wi.js";
6
- import { a as toolExecutionErrorResult, f as registerFeishuChatTools, g as resolveToolsConfig, h as resolveFeishuToolAccount, m as resolveAnyEnabledFeishuToolsConfig, n as registerFeishuDriveTools, o as unknownToolActionResult, p as createFeishuToolClient } from "./drive-Bpe8ONK_.js";
7
+ import { a as toolExecutionErrorResult, f as registerFeishuChatTools, g as resolveToolsConfig, h as resolveFeishuToolAccount, m as resolveAnyEnabledFeishuToolsConfig, n as registerFeishuDriveTools, o as unknownToolActionResult, p as createFeishuToolClient } from "./drive-B4E8OXZ0.js";
7
8
  import { n as getFeishuThreadBindingManager, r as testing, t as createFeishuThreadBindingManager } from "./thread-bindings-V0bwk0A1.js";
8
9
  import { n as handleFeishuSubagentEnded, r as handleFeishuSubagentSpawning, t as handleFeishuSubagentDeliveryTarget } from "./subagent-hooks-BKTOxB-T.js";
9
10
  import { optionalPositiveIntegerSchema } from "openclaw/plugin-sdk/channel-actions";
@@ -12,9 +13,9 @@ import { existsSync } from "node:fs";
12
13
  import { homedir } from "node:os";
13
14
  import { basename, isAbsolute, resolve } from "node:path";
14
15
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
15
- import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
16
16
  import { jsonResult } from "openclaw/plugin-sdk/tool-results";
17
17
  import { Type } from "typebox";
18
+ import { canonicalizeBase64, estimateBase64DecodedBytes, extensionForMime } from "openclaw/plugin-sdk/media-runtime";
18
19
  import { readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers";
19
20
  import { createClackPrompter } from "openclaw/plugin-sdk/setup-runtime";
20
21
  //#region extensions/feishu/src/doc-schema.ts
@@ -565,6 +566,249 @@ async function updateColorText(client, docToken, blockId, content) {
565
566
  };
566
567
  }
567
568
  //#endregion
569
+ //#region extensions/feishu/src/docx-markdown.ts
570
+ const MAX_BREAK_PROBES = 32;
571
+ const STABLE_LINE_CONTAINER_TYPES = /* @__PURE__ */ new Set([
572
+ "list",
573
+ "blockquote",
574
+ "code"
575
+ ]);
576
+ function visitMarkdown(root, visitor) {
577
+ const pending = [root];
578
+ while (pending.length > 0) {
579
+ const node = pending.pop();
580
+ if (!node) continue;
581
+ visitor(node);
582
+ if (!node.children) continue;
583
+ for (let index = node.children.length - 1; index >= 0; index -= 1) {
584
+ const child = node.children[index];
585
+ if (child) pending.push(child);
586
+ }
587
+ }
588
+ }
589
+ function resolveRemoteImageUrl(value) {
590
+ if (!value) return;
591
+ try {
592
+ const url = new URL(value);
593
+ return url.protocol === "http:" || url.protocol === "https:" ? value : void 0;
594
+ } catch {
595
+ return;
596
+ }
597
+ }
598
+ function collectMarkdownImages(root) {
599
+ const definitions = /* @__PURE__ */ new Map();
600
+ visitMarkdown(root, (node) => {
601
+ if (node.type !== "definition" || !node.identifier || !node.url) return;
602
+ if (!definitions.has(node.identifier)) definitions.set(node.identifier, node.url);
603
+ });
604
+ const images = [];
605
+ visitMarkdown(root, (node) => {
606
+ if (node.type === "image") {
607
+ images.push({ url: resolveRemoteImageUrl(node.url) });
608
+ return;
609
+ }
610
+ if (node.type === "imageReference") images.push({ url: resolveRemoteImageUrl(node.identifier ? definitions.get(node.identifier) : void 0) });
611
+ });
612
+ return images;
613
+ }
614
+ function splitSourceAtOffsets(source, offsets) {
615
+ const chunks = [];
616
+ let start = 0;
617
+ for (const offset of offsets) {
618
+ if (offset <= start || offset >= source.length) continue;
619
+ chunks.push(source.slice(start, offset));
620
+ start = offset;
621
+ }
622
+ chunks.push(source.slice(start));
623
+ return chunks;
624
+ }
625
+ function headingOffsets(source, root) {
626
+ const offsets = [];
627
+ for (const node of root.children ?? []) {
628
+ if (node.type !== "heading" || node.depth === void 0 || node.depth > 2) continue;
629
+ const offset = node.position?.start.offset;
630
+ if (offset !== void 0 && source[offset] === "#") offsets.push(offset);
631
+ }
632
+ return offsets;
633
+ }
634
+ function blockBreakOffsets(root) {
635
+ const offsets = [];
636
+ for (const block of root.children ?? []) {
637
+ const blockStart = block.position?.start.offset;
638
+ if (blockStart !== void 0 && blockStart > 0) offsets.push(blockStart);
639
+ }
640
+ return offsets;
641
+ }
642
+ function paragraphBreakOffsets(source, root) {
643
+ const offsets = [];
644
+ for (const block of root.children ?? []) {
645
+ if (block.type !== "paragraph") continue;
646
+ for (const child of block.children ?? []) {
647
+ if (child.type !== "text") continue;
648
+ const start = child.position?.start.offset;
649
+ const end = child.position?.end.offset;
650
+ if (start === void 0 || end === void 0) continue;
651
+ for (let offset = start; offset < end; offset += 1) {
652
+ const char = source[offset];
653
+ if (char === " " || char === " " || char === "\n" || char === "\r") offsets.push(offset + 1);
654
+ }
655
+ }
656
+ }
657
+ return offsets;
658
+ }
659
+ function lineBreakOffsets(source) {
660
+ const offsets = [];
661
+ for (let offset = 0; offset < source.length; offset += 1) if (source[offset] === "\n" && offset + 1 < source.length) offsets.push(offset + 1);
662
+ return offsets;
663
+ }
664
+ function nearestOffset(offsets, target, sourceLength) {
665
+ return offsets.filter((offset) => offset > 0 && offset < sourceLength).toSorted((left, right) => Math.abs(left - target) - Math.abs(right - target) || left - right);
666
+ }
667
+ function isStableParagraphBreak(source, offset) {
668
+ const before = parseFeishuMarkdown(source.slice(0, offset)).children?.at(-1);
669
+ const after = parseFeishuMarkdown(source.slice(offset)).children?.[0];
670
+ return before?.type === "paragraph" && after?.type === "paragraph";
671
+ }
672
+ function isStableContainerBreak(source, offset, containerType) {
673
+ const before = parseFeishuMarkdown(source.slice(0, offset)).children;
674
+ const after = parseFeishuMarkdown(source.slice(offset)).children;
675
+ return before?.length === 1 && after?.length === 1 && before[0]?.type === containerType && after[0]?.type === containerType;
676
+ }
677
+ function splitTableAtRow(source, root, target) {
678
+ const table = root.children?.length === 1 ? root.children[0] : void 0;
679
+ if (table?.type !== "table") return;
680
+ const rows = table.children ?? [];
681
+ const firstBodyOffset = rows[1]?.position?.start.offset;
682
+ const splitOffset = nearestOffset(rows.slice(2).map((row) => row.position?.start.offset).filter((offset) => offset !== void 0), target, source.length)[0];
683
+ if (firstBodyOffset === void 0 || splitOffset === void 0) return;
684
+ const tableStart = table.position?.start.offset ?? 0;
685
+ const repeatedHeader = source.slice(tableStart, firstBodyOffset);
686
+ const chunks = [source.slice(0, splitOffset), `${repeatedHeader}${source.slice(splitOffset)}`];
687
+ if (chunks.every((chunk) => parseFeishuMarkdown(chunk).children?.[0]?.type === "table")) return chunks;
688
+ }
689
+ function isFencedCodeSource(source) {
690
+ const firstLineEnd = source.indexOf("\n");
691
+ const firstLine = source.slice(0, firstLineEnd === -1 ? source.length : firstLineEnd);
692
+ let indent = 0;
693
+ while (indent < firstLine.length && firstLine[indent] === " ") indent += 1;
694
+ if (indent > 3) return false;
695
+ const marker = firstLine.slice(indent);
696
+ return marker.startsWith("```") || marker.startsWith("~~~");
697
+ }
698
+ function createDocxMarkdownChunk(markdown) {
699
+ return {
700
+ markdown,
701
+ images: collectMarkdownImages(parseFeishuMarkdown(markdown))
702
+ };
703
+ }
704
+ function createDocxMarkdownPlan(markdown) {
705
+ return { chunks: splitSourceAtOffsets(markdown, headingOffsets(markdown, parseFeishuMarkdown(markdown))).map(createDocxMarkdownChunk) };
706
+ }
707
+ function splitDocxMarkdownBySize(markdown, maxChars) {
708
+ if (markdown.length <= maxChars) return [markdown];
709
+ const root = parseFeishuMarkdown(markdown);
710
+ const target = Math.min(markdown.length - 1, Math.max(1, maxChars));
711
+ const splitOffset = nearestOffset(blockBreakOffsets(root), target, markdown.length)[0];
712
+ if (splitOffset !== void 0) return [markdown.slice(0, splitOffset), markdown.slice(splitOffset)];
713
+ const paragraphOffset = nearestOffset(paragraphBreakOffsets(markdown, root), target, markdown.length).slice(0, MAX_BREAK_PROBES).find((offset) => isStableParagraphBreak(markdown, offset));
714
+ if (paragraphOffset !== void 0) return [markdown.slice(0, paragraphOffset), markdown.slice(paragraphOffset)];
715
+ if (root.children?.length === 1 && root.children[0]?.type === "code" && isFencedCodeSource(markdown)) return chunkFeishuMarkdown(markdown, maxChars);
716
+ const tableChunks = splitTableAtRow(markdown, root, target);
717
+ if (tableChunks) return tableChunks;
718
+ const containerType = root.children?.length === 1 ? root.children[0]?.type : void 0;
719
+ if (containerType && STABLE_LINE_CONTAINER_TYPES.has(containerType)) {
720
+ const containerOffset = nearestOffset(lineBreakOffsets(markdown), target, markdown.length).slice(0, MAX_BREAK_PROBES).find((offset) => isStableContainerBreak(markdown, offset, containerType));
721
+ if (containerOffset !== void 0) return [markdown.slice(0, containerOffset), markdown.slice(containerOffset)];
722
+ }
723
+ return [markdown];
724
+ }
725
+ //#endregion
726
+ //#region extensions/feishu/src/docx-upload-input.ts
727
+ function decodeBase64Image(params) {
728
+ const estimatedBytes = estimateBase64DecodedBytes(params.payload);
729
+ if (estimatedBytes > params.maxBytes) throw new Error(`${params.oversizedLabel} exceeds limit: estimated ${estimatedBytes} bytes > ${params.maxBytes} bytes`);
730
+ const canonical = canonicalizeBase64(params.payload);
731
+ if (!canonical) throw new Error(params.invalidMessage);
732
+ return Buffer.from(canonical, "base64");
733
+ }
734
+ function resolveDataUriImage(image, maxBytes, fileName) {
735
+ const commaIndex = image.indexOf(",");
736
+ if (commaIndex === -1) throw new Error("Invalid data URI: missing comma separator.");
737
+ const metadata = image.slice(5, commaIndex).split(";");
738
+ if (!metadata.slice(1).map((value) => value.trim().toLowerCase()).includes("base64")) throw new Error("Invalid data URI: missing ';base64' marker. Expected format: data:image/png;base64,<base64data>");
739
+ const buffer = decodeBase64Image({
740
+ payload: image.slice(commaIndex + 1),
741
+ maxBytes,
742
+ invalidMessage: "Invalid data URI: base64 payload is malformed.",
743
+ oversizedLabel: "Image data URI"
744
+ });
745
+ const mime = metadata[0]?.trim();
746
+ const extension = extensionForMime(mime)?.slice(1) ?? "png";
747
+ return {
748
+ buffer,
749
+ fileName: fileName ?? `image.${extension}`
750
+ };
751
+ }
752
+ async function resolveLocalUpload(filePath, maxBytes, localRoots, fileName) {
753
+ return {
754
+ buffer: (await getFeishuRuntime().media.loadWebMedia(resolve(filePath), {
755
+ maxBytes,
756
+ optimizeImages: false,
757
+ localRoots
758
+ })).buffer,
759
+ fileName: fileName ?? basename(filePath)
760
+ };
761
+ }
762
+ function resolveImageLocalPath(image) {
763
+ const candidate = image.startsWith("~") ? `${homedir()}${image.slice(1)}` : image;
764
+ const unambiguousPath = image.startsWith("~") || image.startsWith("./") || image.startsWith("../");
765
+ const absolutePath = isAbsolute(image);
766
+ if (unambiguousPath || absolutePath && existsSync(candidate)) return candidate;
767
+ if (absolutePath) throw new Error(`File not found: "${candidate}". If you intended to pass image binary data, use a data URI instead: data:image/jpeg;base64,...`);
768
+ }
769
+ async function resolveRemoteUpload(input) {
770
+ const fetched = await getFeishuRuntime().channel.media.readRemoteMediaBuffer({
771
+ url: input.url,
772
+ maxBytes: input.maxBytes,
773
+ ...input.remoteReadTimeoutMs !== void 0 ? {
774
+ responseHeaderTimeoutMs: input.remoteReadTimeoutMs,
775
+ readIdleTimeoutMs: input.remoteReadTimeoutMs
776
+ } : {}
777
+ });
778
+ const urlFileName = new URL(input.url).pathname.split("/").pop() || "upload.bin";
779
+ return {
780
+ buffer: fetched.buffer,
781
+ fileName: input.fileName ?? fetched.fileName ?? urlFileName
782
+ };
783
+ }
784
+ async function resolveDocxUploadInput(input) {
785
+ const sources = [
786
+ input.url && "url",
787
+ input.filePath && "file_path",
788
+ input.image && "image"
789
+ ].filter((source) => Boolean(source));
790
+ if (sources.length !== 1) throw new Error(sources.length === 0 ? "Either url, file_path, or image (base64/data URI) must be provided" : `Provide only one upload source; got: ${sources.join(", ")}`);
791
+ if (input.url) return await resolveRemoteUpload({
792
+ ...input,
793
+ url: input.url
794
+ });
795
+ if (input.filePath) return await resolveLocalUpload(input.filePath, input.maxBytes, input.localRoots, input.fileName);
796
+ const image = input.image;
797
+ if (!image) throw new Error("Image input must not be empty");
798
+ if (image.startsWith("data:")) return resolveDataUriImage(image, input.maxBytes, input.fileName);
799
+ const localPath = resolveImageLocalPath(image);
800
+ if (localPath) return await resolveLocalUpload(localPath, input.maxBytes, input.localRoots, input.fileName);
801
+ return {
802
+ buffer: decodeBase64Image({
803
+ payload: image,
804
+ maxBytes: input.maxBytes,
805
+ invalidMessage: "Invalid base64: image input is malformed. Use a data URI (data:image/png;base64,...) or a local file path instead.",
806
+ oversizedLabel: "Base64 image"
807
+ }),
808
+ fileName: input.fileName ?? "image.png"
809
+ };
810
+ }
811
+ //#endregion
568
812
  //#region extensions/feishu/src/docx.ts
569
813
  function resolveDocToolLocalRoots(ctx) {
570
814
  if (ctx.fsPolicy?.workspaceOnly !== true) return;
@@ -572,19 +816,6 @@ function resolveDocToolLocalRoots(ctx) {
572
816
  if (!workspaceDir) return [];
573
817
  return [resolve(workspaceDir)];
574
818
  }
575
- /** Extract image URLs from markdown content */
576
- function extractImageUrls(markdown) {
577
- const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
578
- const urls = [];
579
- let match;
580
- while ((match = regex.exec(markdown)) !== null) {
581
- const capturedUrl = match[1];
582
- if (capturedUrl === void 0) continue;
583
- const url = capturedUrl.trim();
584
- if (url.startsWith("http://") || url.startsWith("https://")) urls.push(url);
585
- }
586
- return urls;
587
- }
588
819
  const BLOCK_TYPE_NAMES = {
589
820
  1: "Page",
590
821
  2: "Text",
@@ -725,83 +956,49 @@ async function insertBlocks(client, docToken, blocks, parentBlockId, index) {
725
956
  skipped
726
957
  };
727
958
  }
728
- /** Split markdown into chunks at top-level headings (# or ##) to stay within API content limits */
729
- function splitMarkdownByHeadings(markdown) {
730
- const lines = markdown.split("\n");
731
- const chunks = [];
732
- let current = [];
733
- let inFencedBlock = false;
734
- for (const line of lines) {
735
- if (/^(`{3,}|~{3,})/.test(line)) inFencedBlock = !inFencedBlock;
736
- if (!inFencedBlock && /^#{1,2}\s/.test(line) && current.length > 0) {
737
- chunks.push(current.join("\n"));
738
- current = [];
739
- }
740
- current.push(line);
741
- }
742
- if (current.length > 0) chunks.push(current.join("\n"));
743
- return chunks;
744
- }
745
- /** Split markdown by size, preferring to break outside fenced code blocks when possible */
746
- function splitMarkdownBySize(markdown, maxChars) {
747
- if (markdown.length <= maxChars) return [markdown];
748
- const lines = markdown.split("\n");
749
- const chunks = [];
750
- let current = [];
751
- let currentLength = 0;
752
- let inFencedBlock = false;
753
- for (const line of lines) {
754
- if (/^(`{3,}|~{3,})/.test(line)) inFencedBlock = !inFencedBlock;
755
- const lineLength = line.length + 1;
756
- const wouldExceed = currentLength + lineLength > maxChars;
757
- if (current.length > 0 && wouldExceed && !inFencedBlock) {
758
- chunks.push(current.join("\n"));
759
- current = [];
760
- currentLength = 0;
761
- }
762
- current.push(line);
763
- currentLength += lineLength;
764
- }
765
- if (current.length > 0) chunks.push(current.join("\n"));
766
- if (chunks.length > 1) return chunks;
767
- const midpoint = Math.floor(lines.length / 2);
768
- if (midpoint <= 0 || midpoint >= lines.length) return [markdown];
769
- return [lines.slice(0, midpoint).join("\n"), lines.slice(midpoint).join("\n")];
770
- }
771
- async function convertMarkdownWithFallback(client, markdown, depth = 0) {
959
+ async function convertMarkdownWithFallback(client, chunk, depth = 0) {
772
960
  try {
773
- return await convertMarkdown(client, markdown);
961
+ return {
962
+ ...await convertMarkdown(client, chunk.markdown),
963
+ images: chunk.images
964
+ };
774
965
  } catch (error) {
775
- if (depth >= MAX_CONVERT_RETRY_DEPTH || markdown.length < 2) throw error;
776
- const chunks = splitMarkdownBySize(markdown, Math.max(256, Math.floor(markdown.length / 2)));
966
+ if (depth >= MAX_CONVERT_RETRY_DEPTH || chunk.markdown.length < 2) throw error;
967
+ const splitTarget = Math.max(256, Math.floor(chunk.markdown.length / 2));
968
+ const chunks = splitDocxMarkdownBySize(chunk.markdown, splitTarget).map(createDocxMarkdownChunk);
777
969
  if (chunks.length <= 1) throw error;
778
970
  const blocks = [];
779
971
  const firstLevelBlockIds = [];
780
- for (const chunk of chunks) {
781
- const converted = await convertMarkdownWithFallback(client, chunk, depth + 1);
972
+ const images = [];
973
+ for (const fallbackChunk of chunks) {
974
+ const converted = await convertMarkdownWithFallback(client, fallbackChunk, depth + 1);
782
975
  blocks.push(...converted.blocks);
783
976
  firstLevelBlockIds.push(...converted.firstLevelBlockIds);
977
+ images.push(...converted.images);
784
978
  }
785
979
  return {
786
980
  blocks,
787
- firstLevelBlockIds
981
+ firstLevelBlockIds,
982
+ images
788
983
  };
789
984
  }
790
985
  }
791
986
  /** Convert markdown in chunks to avoid document.convert content size limits */
792
- async function chunkedConvertMarkdown(client, markdown) {
793
- const chunks = splitMarkdownByHeadings(markdown);
987
+ async function chunkedConvertMarkdown(client, chunks) {
794
988
  const allBlocks = [];
795
989
  const allRootIds = [];
990
+ const allImages = [];
796
991
  for (const chunk of chunks) {
797
- const { blocks, firstLevelBlockIds } = await convertMarkdownWithFallback(client, chunk);
992
+ const { blocks, firstLevelBlockIds, images } = await convertMarkdownWithFallback(client, chunk);
798
993
  const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
799
994
  allBlocks.push(...orderedBlocks);
800
995
  allRootIds.push(...rootIds);
996
+ allImages.push(...images);
801
997
  }
802
998
  return {
803
999
  blocks: allBlocks,
804
- firstLevelBlockIds: allRootIds
1000
+ firstLevelBlockIds: allRootIds,
1001
+ images: allImages
805
1002
  };
806
1003
  }
807
1004
  /**
@@ -859,98 +1056,21 @@ async function uploadImageToDocx(client, blockId, imageBuffer, fileName, docToke
859
1056
  if (!fileToken) throw new Error("Image upload failed: no file_token returned");
860
1057
  return fileToken;
861
1058
  }
862
- async function downloadImage(url, maxBytes) {
863
- return (await getFeishuRuntime().channel.media.readRemoteMediaBuffer({
864
- url,
865
- maxBytes
866
- })).buffer;
867
- }
868
- async function resolveUploadInput(url, filePath, maxBytes, localRoots, explicitFileName, imageInput) {
869
- const inputSources = [
870
- url ? "url" : null,
871
- filePath ? "file_path" : null,
872
- imageInput ? "image" : null
873
- ].filter(Boolean);
874
- if (inputSources.length > 1) throw new Error(`Provide only one image source; got: ${inputSources.join(", ")}`);
875
- if (imageInput?.startsWith("data:")) {
876
- const commaIdx = imageInput.indexOf(",");
877
- if (commaIdx === -1) throw new Error("Invalid data URI: missing comma separator.");
878
- const header = imageInput.slice(0, commaIdx);
879
- const data = imageInput.slice(commaIdx + 1);
880
- if (!header.includes(";base64")) throw new Error("Invalid data URI: missing ';base64' marker. Expected format: data:image/png;base64,<base64data>");
881
- const trimmedData = data.trim();
882
- if (trimmedData.length === 0 || !/^[A-Za-z0-9+/]+=*$/.test(trimmedData)) throw new Error(`Invalid data URI: base64 payload contains characters outside the standard alphabet.`);
883
- const ext = extensionForMime(header.match(/data:([^;]+)/)?.[1])?.slice(1) ?? "png";
884
- const estimatedBytes = Math.ceil(trimmedData.length * 3 / 4);
885
- if (estimatedBytes > maxBytes) throw new Error(`Image data URI exceeds limit: estimated ${estimatedBytes} bytes > ${maxBytes} bytes`);
886
- return {
887
- buffer: Buffer.from(trimmedData, "base64"),
888
- fileName: explicitFileName ?? `image.${ext}`
889
- };
890
- }
891
- if (imageInput) {
892
- const candidate = imageInput.startsWith("~") ? imageInput.replace(/^~/, homedir()) : imageInput;
893
- const unambiguousPath = imageInput.startsWith("~") || imageInput.startsWith("./") || imageInput.startsWith("../");
894
- const absolutePath = isAbsolute(imageInput);
895
- if (unambiguousPath || absolutePath && existsSync(candidate)) {
896
- const resolvedPath = resolve(candidate);
897
- return {
898
- buffer: (await getFeishuRuntime().media.loadWebMedia(resolvedPath, {
899
- maxBytes,
900
- optimizeImages: false,
901
- localRoots
902
- })).buffer,
903
- fileName: explicitFileName ?? basename(candidate)
904
- };
905
- }
906
- if (absolutePath && !existsSync(candidate)) throw new Error(`File not found: "${candidate}". If you intended to pass image binary data, use a data URI instead: data:image/jpeg;base64,...`);
907
- }
908
- if (imageInput) {
909
- const trimmed = imageInput.trim();
910
- if (trimmed.length === 0 || !/^[A-Za-z0-9+/]+=*$/.test(trimmed)) throw new Error("Invalid base64: image input contains characters outside the standard base64 alphabet. Use a data URI (data:image/png;base64,...) or a local file path instead.");
911
- const estimatedBytes = Math.ceil(trimmed.length * 3 / 4);
912
- if (estimatedBytes > maxBytes) throw new Error(`Base64 image exceeds limit: estimated ${estimatedBytes} bytes > ${maxBytes} bytes`);
913
- const buffer = Buffer.from(trimmed, "base64");
914
- if (buffer.length === 0) throw new Error("Base64 image decoded to empty buffer; check the input.");
915
- return {
916
- buffer,
917
- fileName: explicitFileName ?? "image.png"
918
- };
919
- }
920
- if (!url && !filePath) throw new Error("Either url, file_path, or image (base64/data URI) must be provided");
921
- if (url && filePath) throw new Error("Provide only one of url or file_path");
922
- if (url) {
923
- const fetched = await getFeishuRuntime().channel.media.readRemoteMediaBuffer({
924
- url,
925
- maxBytes
926
- });
927
- const guessed = new URL(url).pathname.split("/").pop() || "upload.bin";
928
- return {
929
- buffer: fetched.buffer,
930
- fileName: explicitFileName || guessed
931
- };
932
- }
933
- const resolvedFilePath = resolve(filePath);
934
- return {
935
- buffer: (await getFeishuRuntime().media.loadWebMedia(resolvedFilePath, {
936
- maxBytes,
937
- optimizeImages: false,
938
- localRoots
939
- })).buffer,
940
- fileName: explicitFileName || basename(filePath)
941
- };
942
- }
943
- async function processImages(client, docToken, markdown, insertedBlocks, maxBytes) {
944
- const imageUrls = extractImageUrls(markdown);
945
- if (imageUrls.length === 0) return 0;
1059
+ async function processImages(client, docToken, images, insertedBlocks, maxBytes, imageReadTimeoutMs) {
1060
+ if (images.length === 0) return 0;
946
1061
  const imageBlocks = insertedBlocks.filter((b) => b.block_type === 27);
947
1062
  let processed = 0;
948
- for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
949
- const url = imageUrls[i];
1063
+ for (let i = 0; i < Math.min(images.length, imageBlocks.length); i++) {
1064
+ const url = images[i]?.url;
950
1065
  const blockId = imageBlocks[i]?.block_id;
951
1066
  if (!url || !blockId) continue;
952
1067
  try {
953
- const fileToken = await uploadImageToDocx(client, blockId, await downloadImage(url, maxBytes), new URL(url).pathname.split("/").pop() || `image_${i}.png`, docToken);
1068
+ const upload = await resolveDocxUploadInput({
1069
+ url,
1070
+ maxBytes,
1071
+ remoteReadTimeoutMs: imageReadTimeoutMs
1072
+ });
1073
+ const fileToken = await uploadImageToDocx(client, blockId, upload.buffer, upload.fileName, docToken);
954
1074
  await client.docx.documentBlock.patch({
955
1075
  path: {
956
1076
  document_id: docToken,
@@ -965,7 +1085,16 @@ async function processImages(client, docToken, markdown, insertedBlocks, maxByte
965
1085
  }
966
1086
  return processed;
967
1087
  }
968
- async function uploadImageBlock(client, docToken, maxBytes, localRoots, url, filePath, parentBlockId, filename, index, imageInput) {
1088
+ async function uploadImageBlock(client, docToken, maxBytes, imageReadTimeoutMs, localRoots, url, filePath, parentBlockId, filename, index, imageInput) {
1089
+ const upload = await resolveDocxUploadInput({
1090
+ url,
1091
+ filePath,
1092
+ image: imageInput,
1093
+ maxBytes,
1094
+ localRoots,
1095
+ fileName: filename,
1096
+ remoteReadTimeoutMs: imageReadTimeoutMs
1097
+ });
969
1098
  const insertRes = await client.docx.documentBlockChildren.create({
970
1099
  path: {
971
1100
  document_id: docToken,
@@ -983,7 +1112,6 @@ async function uploadImageBlock(client, docToken, maxBytes, localRoots, url, fil
983
1112
  if (insertRes.code !== 0) throw new Error(`Failed to create image block: ${insertRes.msg}`);
984
1113
  const imageBlockId = insertRes.data?.children?.find((b) => b.block_type === 27)?.block_id;
985
1114
  if (!imageBlockId) throw new Error("Failed to create image block");
986
- const upload = await resolveUploadInput(url, filePath, maxBytes, localRoots, filename, imageInput);
987
1115
  const fileToken = await uploadImageToDocx(client, imageBlockId, upload.buffer, upload.fileName, docToken);
988
1116
  const patchRes = await client.docx.documentBlock.patch({
989
1117
  path: {
@@ -1003,8 +1131,14 @@ async function uploadImageBlock(client, docToken, maxBytes, localRoots, url, fil
1003
1131
  }
1004
1132
  async function uploadFileBlock(client, docToken, maxBytes, localRoots, url, filePath, parentBlockId, filename) {
1005
1133
  const blockId = parentBlockId ?? docToken;
1006
- const upload = await resolveUploadInput(url, filePath, maxBytes, localRoots, filename);
1007
- const converted = await convertMarkdown(client, `[${upload.fileName}](https://example.com/placeholder)`);
1134
+ const upload = await resolveDocxUploadInput({
1135
+ url,
1136
+ filePath,
1137
+ maxBytes,
1138
+ localRoots,
1139
+ fileName: filename
1140
+ });
1141
+ const converted = await convertMarkdown(client, "[file](https://example.com/placeholder)");
1008
1142
  const { orderedBlocks } = normalizeConvertedBlockTree(converted.blocks, converted.firstLevelBlockIds);
1009
1143
  const { children: inserted } = await insertBlocks(client, docToken, orderedBlocks, blockId);
1010
1144
  const placeholderBlock = inserted[0];
@@ -1128,10 +1262,11 @@ async function createDoc(client, title, folderToken, options) {
1128
1262
  }
1129
1263
  };
1130
1264
  }
1131
- async function writeDoc(client, docToken, markdown, maxBytes, logger) {
1132
- const deleted = await clearDocumentContent(client, docToken);
1265
+ async function writeDoc(client, docToken, markdown, maxBytes, imageReadTimeoutMs, logger) {
1266
+ const markdownPlan = createDocxMarkdownPlan(markdown);
1133
1267
  logger?.info?.("feishu_doc: Converting markdown...");
1134
- const { blocks, firstLevelBlockIds } = await chunkedConvertMarkdown(client, markdown);
1268
+ const { blocks, firstLevelBlockIds, images } = await chunkedConvertMarkdown(client, markdownPlan.chunks);
1269
+ const deleted = await clearDocumentContent(client, docToken);
1135
1270
  if (blocks.length === 0) return {
1136
1271
  success: true,
1137
1272
  blocks_deleted: deleted,
@@ -1141,7 +1276,7 @@ async function writeDoc(client, docToken, markdown, maxBytes, logger) {
1141
1276
  logger?.info?.(`feishu_doc: Converted to ${blocks.length} blocks, inserting...`);
1142
1277
  const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
1143
1278
  const { children: inserted } = blocks.length > 1e3 ? await insertBlocksInBatches(client, docToken, orderedBlocks, rootIds, logger) : await insertBlocksWithDescendant(client, docToken, orderedBlocks, rootIds);
1144
- const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
1279
+ const imagesProcessed = await processImages(client, docToken, images, inserted, maxBytes, imageReadTimeoutMs);
1145
1280
  logger?.info?.(`feishu_doc: Done (${blocks.length} blocks, ${imagesProcessed} images)`);
1146
1281
  return {
1147
1282
  success: true,
@@ -1150,14 +1285,15 @@ async function writeDoc(client, docToken, markdown, maxBytes, logger) {
1150
1285
  images_processed: imagesProcessed
1151
1286
  };
1152
1287
  }
1153
- async function appendDoc(client, docToken, markdown, maxBytes, logger) {
1288
+ async function appendDoc(client, docToken, markdown, maxBytes, imageReadTimeoutMs, logger) {
1289
+ const markdownPlan = createDocxMarkdownPlan(markdown);
1154
1290
  logger?.info?.("feishu_doc: Converting markdown...");
1155
- const { blocks, firstLevelBlockIds } = await chunkedConvertMarkdown(client, markdown);
1291
+ const { blocks, firstLevelBlockIds, images } = await chunkedConvertMarkdown(client, markdownPlan.chunks);
1156
1292
  if (blocks.length === 0) throw new Error("Content is empty");
1157
1293
  logger?.info?.(`feishu_doc: Converted to ${blocks.length} blocks, inserting...`);
1158
1294
  const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
1159
1295
  const { children: inserted } = blocks.length > 1e3 ? await insertBlocksInBatches(client, docToken, orderedBlocks, rootIds, logger) : await insertBlocksWithDescendant(client, docToken, orderedBlocks, rootIds);
1160
- const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
1296
+ const imagesProcessed = await processImages(client, docToken, images, inserted, maxBytes, imageReadTimeoutMs);
1161
1297
  logger?.info?.(`feishu_doc: Done (${blocks.length} blocks, ${imagesProcessed} images)`);
1162
1298
  return {
1163
1299
  success: true,
@@ -1166,7 +1302,8 @@ async function appendDoc(client, docToken, markdown, maxBytes, logger) {
1166
1302
  block_ids: inserted.map((b) => b.block_id)
1167
1303
  };
1168
1304
  }
1169
- async function insertDoc(client, docToken, markdown, afterBlockId, maxBytes, logger) {
1305
+ async function insertDoc(client, docToken, markdown, afterBlockId, maxBytes, imageReadTimeoutMs, logger) {
1306
+ const markdownPlan = createDocxMarkdownPlan(markdown);
1170
1307
  const blockInfo = await client.docx.documentBlock.get({ path: {
1171
1308
  document_id: docToken,
1172
1309
  block_id: afterBlockId
@@ -1191,7 +1328,7 @@ async function insertDoc(client, docToken, markdown, afterBlockId, maxBytes, log
1191
1328
  if (blockIndex === -1) throw new Error(`after_block_id "${afterBlockId}" was not found among the children of parent block "${parentId}". Use list_blocks to verify the block ID.`);
1192
1329
  const insertIndex = blockIndex + 1;
1193
1330
  logger?.info?.("feishu_doc: Converting markdown...");
1194
- const { blocks, firstLevelBlockIds } = await chunkedConvertMarkdown(client, markdown);
1331
+ const { blocks, firstLevelBlockIds, images } = await chunkedConvertMarkdown(client, markdownPlan.chunks);
1195
1332
  if (blocks.length === 0) throw new Error("Content is empty");
1196
1333
  const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
1197
1334
  logger?.info?.(`feishu_doc: Converted to ${blocks.length} blocks, inserting at index ${insertIndex}...`);
@@ -1199,7 +1336,7 @@ async function insertDoc(client, docToken, markdown, afterBlockId, maxBytes, log
1199
1336
  parentBlockId: parentId,
1200
1337
  index: insertIndex
1201
1338
  });
1202
- const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
1339
+ const imagesProcessed = await processImages(client, docToken, images, inserted, maxBytes, imageReadTimeoutMs);
1203
1340
  logger?.info?.(`feishu_doc: Done (${blocks.length} blocks, ${imagesProcessed} images)`);
1204
1341
  return {
1205
1342
  success: true,
@@ -1409,6 +1546,15 @@ function registerFeishuDocTools(api) {
1409
1546
  label: "Doc"
1410
1547
  }
1411
1548
  }).config?.mediaMaxMb ?? 30) * 1024 * 1024;
1549
+ const getImageReadTimeoutMs = (params, defaultAccountId) => resolveConfiguredHttpTimeoutMs(resolveFeishuToolAccount({
1550
+ api,
1551
+ executeParams: params,
1552
+ defaultAccountId,
1553
+ requiredTool: {
1554
+ family: "doc",
1555
+ label: "Doc"
1556
+ }
1557
+ }));
1412
1558
  if (toolsCfg.doc) {
1413
1559
  api.registerTool((ctx) => {
1414
1560
  const defaultAccountId = ctx.agentAccountId;
@@ -1425,9 +1571,9 @@ function registerFeishuDocTools(api) {
1425
1571
  const client = getClient(p, defaultAccountId);
1426
1572
  switch (p.action) {
1427
1573
  case "read": return jsonResult(await readDoc(client, p.doc_token));
1428
- case "write": return jsonResult(await writeDoc(client, p.doc_token, p.content, getMediaMaxBytes(p, defaultAccountId), api.logger));
1429
- case "append": return jsonResult(await appendDoc(client, p.doc_token, p.content, getMediaMaxBytes(p, defaultAccountId), api.logger));
1430
- case "insert": return jsonResult(await insertDoc(client, p.doc_token, p.content, p.after_block_id, getMediaMaxBytes(p, defaultAccountId), api.logger));
1574
+ case "write": return jsonResult(await writeDoc(client, p.doc_token, p.content, getMediaMaxBytes(p, defaultAccountId), getImageReadTimeoutMs(p, defaultAccountId), api.logger));
1575
+ case "append": return jsonResult(await appendDoc(client, p.doc_token, p.content, getMediaMaxBytes(p, defaultAccountId), getImageReadTimeoutMs(p, defaultAccountId), api.logger));
1576
+ case "insert": return jsonResult(await insertDoc(client, p.doc_token, p.content, p.after_block_id, getMediaMaxBytes(p, defaultAccountId), getImageReadTimeoutMs(p, defaultAccountId), api.logger));
1431
1577
  case "create": return jsonResult(await createDoc(client, p.title, p.folder_token, {
1432
1578
  grantToRequester: p.grant_to_requester,
1433
1579
  requesterOpenId: trustedRequesterOpenId
@@ -1439,7 +1585,7 @@ function registerFeishuDocTools(api) {
1439
1585
  case "create_table": return jsonResult(await createTable(client, p.doc_token, p.row_size, p.column_size, p.parent_block_id, p.column_width));
1440
1586
  case "write_table_cells": return jsonResult(await writeTableCells(client, p.doc_token, p.table_block_id, p.values));
1441
1587
  case "create_table_with_values": return jsonResult(await createTableWithValues(client, p.doc_token, p.row_size, p.column_size, p.values, p.parent_block_id, p.column_width));
1442
- case "upload_image": return jsonResult(await uploadImageBlock(client, p.doc_token, getMediaMaxBytes(p, defaultAccountId), mediaLocalRoots, p.url, p.file_path, p.parent_block_id, p.filename, p.index, p.image));
1588
+ case "upload_image": return jsonResult(await uploadImageBlock(client, p.doc_token, getMediaMaxBytes(p, defaultAccountId), getImageReadTimeoutMs(p, defaultAccountId), mediaLocalRoots, p.url, p.file_path, p.parent_block_id, p.filename, p.index, p.image));
1443
1589
  case "upload_file": return jsonResult(await uploadFileBlock(client, p.doc_token, getMediaMaxBytes(p, defaultAccountId), mediaLocalRoots, p.url, p.file_path, p.parent_block_id, p.filename));
1444
1590
  case "color_text": return jsonResult(await updateColorText(client, p.doc_token, p.block_id, p.content));
1445
1591
  case "insert_table_row": return jsonResult(await insertTableRow(client, p.doc_token, p.block_id, p.row_index));