This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
openclaw/npm/projects/openclaw-feishu-dc69f44688/node_modules/@openclaw/feishu/dist/api.js
T

2378 lines
88 KiB
JavaScript

import { r as listEnabledFeishuAccounts } from "./accounts-Cfzht2Hc.js";
import { a as setFeishuNamedAccountEnabled, i as feishuSetupAdapter, n as feishuSetupWizard, r as runFeishuLogin, t as feishuPlugin } from "./channel-uEJPBCR9.js";
import { a as parseFeishuTargetId, i as parseFeishuDirectConversationId, n as buildFeishuModelOverrideParentCandidates, r as parseFeishuConversationId, t as buildFeishuConversationId } from "./conversation-id-DuL575sn.js";
import { t as getFeishuRuntime } from "./runtime-C5JxBWZp.js";
import { r as createFeishuClient } from "./client-BwgAJuvo.js";
import { a as jsonToolResult, d as registerFeishuChatTools, f as createFeishuToolClient, h as resolveToolsConfig, m as resolveFeishuToolAccount, n as registerFeishuDriveTools, o as toolExecutionErrorResult, p as resolveAnyEnabledFeishuToolsConfig, s as unknownToolActionResult } from "./drive-djUl8IRC.js";
import { n as getFeishuThreadBindingManager, r as testing, t as createFeishuThreadBindingManager } from "./thread-bindings-V0bwk0A1.js";
import { n as handleFeishuSubagentEnded, r as handleFeishuSubagentSpawning, t as handleFeishuSubagentDeliveryTarget } from "./subagent-hooks-1pqt5tAA.js";
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, readStringValue, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { optionalPositiveIntegerSchema } from "openclaw/plugin-sdk/channel-actions";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { basename, isAbsolute, resolve } from "node:path";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
import { Type } from "typebox";
import { readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers";
import { createClackPrompter } from "openclaw/plugin-sdk/setup-runtime";
//#region extensions/feishu/src/doc-schema.ts
const tableCreationProperties = {
doc_token: Type.String({ description: "Document token" }),
parent_block_id: Type.Optional(Type.String({ description: "Parent block ID (default: document root)" })),
row_size: Type.Integer({
description: "Table row count",
minimum: 1
}),
column_size: Type.Integer({
description: "Table column count",
minimum: 1
}),
column_width: Type.Optional(Type.Array(Type.Number({ minimum: 1 }), { description: "Column widths in px (length should match column_size)" }))
};
const FeishuDocSchema = Type.Union([
Type.Object({
action: Type.Literal("read"),
doc_token: Type.String({ description: "Document token (extract from URL /docx/XXX)" })
}),
Type.Object({
action: Type.Literal("write"),
doc_token: Type.String({ description: "Document token" }),
content: Type.String({ description: "Markdown content to write (replaces entire document content)" })
}),
Type.Object({
action: Type.Literal("append"),
doc_token: Type.String({ description: "Document token" }),
content: Type.String({ description: "Markdown content to append to end of document" })
}),
Type.Object({
action: Type.Literal("insert"),
doc_token: Type.String({ description: "Document token" }),
content: Type.String({ description: "Markdown content to insert" }),
after_block_id: Type.String({ description: "Insert content after this block ID. Use list_blocks to find block IDs." })
}),
Type.Object({
action: Type.Literal("create"),
title: Type.String({ description: "Document title" }),
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
grant_to_requester: Type.Optional(Type.Boolean({ description: "Grant edit permission to the trusted requesting Feishu user from runtime context (default: true)." }))
}),
Type.Object({
action: Type.Literal("list_blocks"),
doc_token: Type.String({ description: "Document token" })
}),
Type.Object({
action: Type.Literal("get_block"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Block ID (from list_blocks)" })
}),
Type.Object({
action: Type.Literal("update_block"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Block ID (from list_blocks)" }),
content: Type.String({ description: "New text content" })
}),
Type.Object({
action: Type.Literal("delete_block"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Block ID" })
}),
Type.Object({
action: Type.Literal("create_table"),
...tableCreationProperties
}),
Type.Object({
action: Type.Literal("write_table_cells"),
doc_token: Type.String({ description: "Document token" }),
table_block_id: Type.String({ description: "Table block ID" }),
values: Type.Array(Type.Array(Type.String()), {
description: "2D matrix values[row][col] to write into table cells",
minItems: 1
})
}),
Type.Object({
action: Type.Literal("create_table_with_values"),
...tableCreationProperties,
values: Type.Array(Type.Array(Type.String()), {
description: "2D matrix values[row][col] to write into table cells",
minItems: 1
})
}),
Type.Object({
action: Type.Literal("insert_table_row"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Table block ID" }),
row_index: Type.Optional(Type.Number({ description: "Row index to insert at (-1 for end, default: -1)" }))
}),
Type.Object({
action: Type.Literal("insert_table_column"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Table block ID" }),
column_index: Type.Optional(Type.Number({ description: "Column index to insert at (-1 for end, default: -1)" }))
}),
Type.Object({
action: Type.Literal("delete_table_rows"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Table block ID" }),
row_start: Type.Number({ description: "Start row index (0-based)" }),
row_count: Type.Optional(Type.Number({ description: "Number of rows to delete (default: 1)" }))
}),
Type.Object({
action: Type.Literal("delete_table_columns"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Table block ID" }),
column_start: Type.Number({ description: "Start column index (0-based)" }),
column_count: Type.Optional(Type.Number({ description: "Number of columns to delete (default: 1)" }))
}),
Type.Object({
action: Type.Literal("merge_table_cells"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Table block ID" }),
row_start: Type.Number({ description: "Start row index" }),
row_end: Type.Number({ description: "End row index (exclusive)" }),
column_start: Type.Number({ description: "Start column index" }),
column_end: Type.Number({ description: "End column index (exclusive)" })
}),
Type.Object({
action: Type.Literal("upload_image"),
doc_token: Type.String({ description: "Document token" }),
url: Type.Optional(Type.String({ description: "Remote image URL (http/https)" })),
file_path: Type.Optional(Type.String({ description: "Local image file path" })),
image: Type.Optional(Type.String({ description: "Image as data URI (data:image/png;base64,...) or plain base64 string. Use instead of url/file_path for DALL-E outputs, canvas screenshots, etc." })),
parent_block_id: Type.Optional(Type.String({ description: "Parent block ID (default: document root)" })),
filename: Type.Optional(Type.String({ description: "Optional filename override" })),
index: Type.Optional(Type.Integer({
minimum: 0,
description: "Insert position (0-based index among siblings). Omit to append."
}))
}),
Type.Object({
action: Type.Literal("upload_file"),
doc_token: Type.String({ description: "Document token" }),
url: Type.Optional(Type.String({ description: "Remote file URL (http/https)" })),
file_path: Type.Optional(Type.String({ description: "Local file path" })),
parent_block_id: Type.Optional(Type.String({ description: "Parent block ID (default: document root)" })),
filename: Type.Optional(Type.String({ description: "Optional filename override" }))
}),
Type.Object({
action: Type.Literal("color_text"),
doc_token: Type.String({ description: "Document token" }),
block_id: Type.String({ description: "Text block ID to update" }),
content: Type.String({ description: "Text with color markup. Tags: [red], [green], [blue], [orange], [yellow], [purple], [grey], [bold], [bg:yellow]. Example: \"Revenue [green]+15%[/green] YoY\"" })
})
]);
//#endregion
//#region extensions/feishu/src/docx-table-ops.ts
const MIN_COLUMN_WIDTH = 50;
const MAX_COLUMN_WIDTH = 400;
const DEFAULT_TABLE_WIDTH = 730;
/**
* Calculate adaptive column widths based on cell content length.
*
* Algorithm:
* 1. For each column, find the max content length across all rows
* 2. Weight CJK characters as 2x width (they render wider)
* 3. Calculate proportional widths based on content length
* 4. Apply min/max constraints
* 5. Redistribute remaining space to fill total table width
*
* Total width is derived from the original column_width values returned
* by the Convert API, ensuring tables match Feishu's expected dimensions.
*
* @param blocks - Array of blocks from Convert API
* @param tableBlockId - The block_id of the table block
* @returns Array of column widths in pixels
*/
function normalizeChildBlockIds(children) {
if (Array.isArray(children)) return children;
return typeof children === "string" ? [children] : [];
}
function omitParentId(block) {
const cleanBlock = { ...block };
delete cleanBlock.parent_id;
return cleanBlock;
}
function createDescendantTable(table, adaptiveWidths) {
const { row_size, column_size } = table.property || {};
return { property: {
row_size,
column_size,
...adaptiveWidths?.length ? { column_width: adaptiveWidths } : {}
} };
}
function calculateAdaptiveColumnWidths(blocks, tableBlockId) {
const tableBlock = blocks.find((b) => b.block_id === tableBlockId && b.block_type === 31);
if (!tableBlock?.table?.property) return [];
const { row_size, column_size, column_width: originalWidths } = tableBlock.table.property;
if (!row_size || !column_size) return [];
const totalWidth = originalWidths && originalWidths.length > 0 ? originalWidths.reduce((a, b) => a + b, 0) : DEFAULT_TABLE_WIDTH;
const cellIds = normalizeChildBlockIds(tableBlock.children);
const blockMap = /* @__PURE__ */ new Map();
for (const block of blocks) if (block.block_id) blockMap.set(block.block_id, block);
function getCellText(cellId) {
const cell = blockMap.get(cellId);
let text = "";
const childIds = normalizeChildBlockIds(cell?.children);
for (const childId of childIds) {
const child = blockMap.get(childId);
if (child?.text?.elements) {
for (const elem of child.text.elements) if (elem.text_run?.content) text += elem.text_run.content;
}
}
return text;
}
function getWeightedLength(text) {
return Array.from(text).reduce((sum, char) => {
return sum + (char.charCodeAt(0) > 255 ? 2 : 1);
}, 0);
}
const maxLengths = Array.from({ length: column_size }, () => 0);
for (let row = 0; row < row_size; row++) for (let col = 0; col < column_size; col++) {
const cellId = cellIds[row * column_size + col];
if (cellId) {
const length = getWeightedLength(getCellText(cellId));
maxLengths[col] = Math.max(maxLengths[col], length);
}
}
const totalLength = maxLengths.reduce((a, b) => a + b, 0);
if (totalLength === 0) {
const equalWidth = Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, Math.floor(totalWidth / column_size)));
return Array.from({ length: column_size }, () => equalWidth);
}
let widths = maxLengths.map((len) => {
const proportion = len / totalLength;
return Math.round(proportion * totalWidth);
});
widths = widths.map((w) => Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, w)));
let remaining = totalWidth - widths.reduce((a, b) => a + b, 0);
while (remaining > 0) {
const growable = widths.map((w, i) => w < MAX_COLUMN_WIDTH ? i : -1).filter((i) => i >= 0);
if (growable.length === 0) break;
const perColumn = Math.floor(remaining / growable.length);
if (perColumn === 0) break;
for (const i of growable) {
const add = Math.min(perColumn, MAX_COLUMN_WIDTH - widths[i]);
widths[i] += add;
remaining -= add;
}
}
return widths;
}
/**
* Clean blocks for Descendant API with adaptive column widths.
*
* - Removes parent_id from all blocks
* - Fixes children type (string → array) for TableCell blocks
* - Removes merge_info (read-only, causes API error)
* - Calculates and applies adaptive column_width for tables
*
* @param blocks - Array of blocks from Convert API
* @returns Cleaned blocks ready for Descendant API
*/
function cleanBlocksForDescendant(blocks) {
const tableWidths = /* @__PURE__ */ new Map();
for (const block of blocks) if (block.block_type === 31 && block.block_id) {
const widths = calculateAdaptiveColumnWidths(blocks, block.block_id);
tableWidths.set(block.block_id, widths);
}
return blocks.map((block) => {
const cleanBlock = omitParentId(block);
if (cleanBlock.block_type === 32 && typeof cleanBlock.children === "string") cleanBlock.children = [cleanBlock.children];
if (cleanBlock.block_type === 31 && cleanBlock.table) {
const adaptiveWidths = block.block_id ? tableWidths.get(block.block_id) : void 0;
cleanBlock.table = createDescendantTable(cleanBlock.table, adaptiveWidths);
}
return cleanBlock;
});
}
async function insertTableRow(client, docToken, blockId, rowIndex = -1) {
const res = await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: blockId
},
data: { insert_table_row: { row_index: rowIndex } }
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
block: res.data?.block
};
}
async function insertTableColumn(client, docToken, blockId, columnIndex = -1) {
const res = await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: blockId
},
data: { insert_table_column: { column_index: columnIndex } }
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
block: res.data?.block
};
}
async function deleteTableRows(client, docToken, blockId, rowStart, rowCount = 1) {
const res = await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: blockId
},
data: { delete_table_rows: {
row_start_index: rowStart,
row_end_index: rowStart + rowCount
} }
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
rows_deleted: rowCount,
block: res.data?.block
};
}
async function deleteTableColumns(client, docToken, blockId, columnStart, columnCount = 1) {
const res = await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: blockId
},
data: { delete_table_columns: {
column_start_index: columnStart,
column_end_index: columnStart + columnCount
} }
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
columns_deleted: columnCount,
block: res.data?.block
};
}
async function mergeTableCells(client, docToken, blockId, rowStart, rowEnd, columnStart, columnEnd) {
const res = await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: blockId
},
data: { merge_table_cells: {
row_start_index: rowStart,
row_end_index: rowEnd,
column_start_index: columnStart,
column_end_index: columnEnd
} }
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
block: res.data?.block
};
}
//#endregion
//#region extensions/feishu/src/docx-batch-insert.ts
const BATCH_SIZE = 1e3;
function normalizeChildIds$1(children) {
if (Array.isArray(children)) return children;
const child = readStringValue(children);
return child ? [child] : void 0;
}
function toDescendantBlock$1(block) {
const children = normalizeChildIds$1(block.children);
return {
...block,
...children ? { children } : {}
};
}
/**
* Collect all descendant blocks for a given first-level block ID.
* Recursively traverses the block tree to gather all children.
*/
function collectDescendants(blockMap, rootId) {
const result = [];
const visited = /* @__PURE__ */ new Set();
function collect(blockId) {
if (visited.has(blockId)) return;
visited.add(blockId);
const block = blockMap.get(blockId);
if (!block) return;
result.push(block);
const children = block.children;
if (Array.isArray(children)) for (const childId of children) collect(childId);
else if (typeof children === "string") collect(children);
}
collect(rootId);
return result;
}
/**
* Insert a single batch of blocks using Descendant API.
*
* @param parentBlockId - Parent block to insert into (defaults to docToken)
* @param index - Position within parent's children (-1 = end)
*/
async function insertBatch(client, docToken, blocks, firstLevelBlockIds, parentBlockId = docToken, index = -1) {
const descendants = cleanBlocksForDescendant(blocks);
if (descendants.length === 0) return [];
const res = await client.docx.documentBlockDescendant.create({
path: {
document_id: docToken,
block_id: parentBlockId
},
data: {
children_id: firstLevelBlockIds,
descendants: descendants.map(toDescendantBlock$1),
index
}
});
if (res.code !== 0) throw new Error(`${res.msg} (code: ${res.code})`);
return res.data?.children ?? [];
}
/**
* Insert blocks in batches for large documents (>1000 blocks).
*
* Batches are split to ensure BOTH children_id AND descendants
* arrays stay under the 1000 block API limit.
*
* @param client - Feishu API client
* @param docToken - Document ID
* @param blocks - All blocks from Convert API
* @param firstLevelBlockIds - IDs of top-level blocks to insert
* @param logger - Optional logger for progress updates
* @param parentBlockId - Parent block to insert into (defaults to docToken = document root)
* @param startIndex - Starting position within parent (-1 = end). For multi-batch inserts,
* each batch advances this by the number of first-level IDs inserted so far.
* @returns Inserted children blocks and any skipped block IDs
*/
async function insertBlocksInBatches(client, docToken, blocks, firstLevelBlockIds, logger, parentBlockId = docToken, startIndex = -1) {
const allChildren = [];
const batches = [];
let currentBatch = {
firstLevelIds: [],
blocks: []
};
const usedBlockIds = /* @__PURE__ */ new Set();
const blockMap = /* @__PURE__ */ new Map();
for (const block of blocks) if (block.block_id) blockMap.set(block.block_id, block);
for (const firstLevelId of firstLevelBlockIds) {
const newBlocks = collectDescendants(blockMap, firstLevelId).filter((b) => b.block_id && !usedBlockIds.has(b.block_id));
if (newBlocks.length > 1e3) throw new Error(`Block "${firstLevelId}" has ${newBlocks.length} descendants, which exceeds the Feishu API limit of ${BATCH_SIZE} blocks per request. Please split the content into smaller sections.`);
if (currentBatch.blocks.length + newBlocks.length > 1e3 && currentBatch.blocks.length > 0) {
batches.push(currentBatch);
currentBatch = {
firstLevelIds: [],
blocks: []
};
}
currentBatch.firstLevelIds.push(firstLevelId);
for (const block of newBlocks) {
currentBatch.blocks.push(block);
if (block.block_id) usedBlockIds.add(block.block_id);
}
}
if (currentBatch.blocks.length > 0) batches.push(currentBatch);
let currentIndex = startIndex;
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
logger?.info?.(`feishu_doc: Inserting batch ${i + 1}/${batches.length} (${batch.blocks.length} blocks)...`);
const children = await insertBatch(client, docToken, batch.blocks, batch.firstLevelIds, parentBlockId, currentIndex);
allChildren.push(...children);
if (currentIndex !== -1) currentIndex += batch.firstLevelIds.length;
}
return {
children: allChildren,
skipped: []
};
}
//#endregion
//#region extensions/feishu/src/docx-color-text.ts
const TEXT_COLOR = {
red: 1,
orange: 2,
yellow: 3,
green: 4,
blue: 5,
purple: 6,
grey: 7,
gray: 7
};
const BACKGROUND_COLOR = {
red: 1,
orange: 2,
yellow: 3,
green: 4,
blue: 5,
purple: 6,
grey: 7,
gray: 7
};
/**
* Parse color markup into segments.
*
* Supports:
* [red]text[/red] → red text
* [bg:yellow]text[/bg] → yellow background
* [bold]text[/bold] → bold
* [green bold]text[/green] → green + bold
*/
function parseColorMarkup(content) {
const segments = [];
const KNOWN = "(?:bg:[a-z]+|bold|red|orange|yellow|green|blue|purple|gr[ae]y)";
const tagPattern = new RegExp(`\\[(${KNOWN}(?:\\s+${KNOWN})*)\\](.*?)\\[\\/(?:[^\\]]+)\\]|([^[]+|\\[)`, "gis");
let match;
while ((match = tagPattern.exec(content)) !== null) if (match[3] !== void 0) {
if (match[3]) segments.push({ text: match[3] });
} else {
const tagStr = normalizeLowercaseStringOrEmpty(match[1]);
const text = match[2];
const tags = tagStr.split(/\s+/);
const segment = { text };
for (const tag of tags) if (tag.startsWith("bg:")) {
const color = tag.slice(3);
if (BACKGROUND_COLOR[color]) segment.bgColor = BACKGROUND_COLOR[color];
} else if (tag === "bold") segment.bold = true;
else if (TEXT_COLOR[tag]) segment.textColor = TEXT_COLOR[tag];
if (text) segments.push(segment);
}
return segments;
}
/**
* Update a text block with colored segments.
*/
async function updateColorText(client, docToken, blockId, content) {
const segments = parseColorMarkup(content);
const elements = segments.map((seg) => ({ text_run: {
content: seg.text,
text_element_style: {
...seg.textColor && { text_color: seg.textColor },
...seg.bgColor && { background_color: seg.bgColor },
...seg.bold && { bold: true }
}
} }));
const res = await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: blockId
},
data: { update_text_elements: { elements } }
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
segments: segments.length,
block: res.data?.block
};
}
//#endregion
//#region extensions/feishu/src/docx.ts
function json$1(data) {
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}],
details: data
};
}
function resolveDocToolLocalRoots(ctx) {
if (ctx.fsPolicy?.workspaceOnly !== true) return;
const workspaceDir = ctx.workspaceDir?.trim();
if (!workspaceDir) return [];
return [resolve(workspaceDir)];
}
/** Extract image URLs from markdown content */
function extractImageUrls(markdown) {
const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
const urls = [];
let match;
while ((match = regex.exec(markdown)) !== null) {
const url = match[1].trim();
if (url.startsWith("http://") || url.startsWith("https://")) urls.push(url);
}
return urls;
}
const BLOCK_TYPE_NAMES = {
1: "Page",
2: "Text",
3: "Heading1",
4: "Heading2",
5: "Heading3",
12: "Bullet",
13: "Ordered",
14: "Code",
15: "Quote",
17: "Todo",
18: "Bitable",
21: "Diagram",
22: "Divider",
23: "File",
27: "Image",
30: "Sheet",
31: "Table",
32: "TableCell"
};
const UNSUPPORTED_CREATE_TYPES = new Set([31, 32]);
/** Clean blocks for insertion (remove unsupported types and read-only fields) */
function cleanBlocksForInsert(blocks) {
const skipped = [];
return {
cleaned: blocks.filter((block) => {
if (UNSUPPORTED_CREATE_TYPES.has(block.block_type)) {
const typeName = BLOCK_TYPE_NAMES[block.block_type] || `type_${block.block_type}`;
skipped.push(typeName);
return false;
}
return true;
}).map((block) => {
if (block.block_type === 31 && block.table?.merge_info) {
const { merge_info: _merge_info, ...tableRest } = block.table;
return Object.assign({}, block, { table: tableRest });
}
return block;
}),
skipped
};
}
/** Max blocks per documentBlockChildren.create request */
const MAX_CONVERT_RETRY_DEPTH = 8;
async function convertMarkdown(client, markdown) {
const res = await client.docx.document.convert({ data: {
content_type: "markdown",
content: markdown
} });
if (res.code !== 0) throw new Error(res.msg);
return {
blocks: res.data?.blocks ?? [],
firstLevelBlockIds: res.data?.first_level_block_ids ?? []
};
}
function normalizeChildIds(children) {
if (Array.isArray(children)) return children.filter((child) => typeof child === "string");
if (typeof children === "string") return [children];
return [];
}
function toCreateChildBlock(block) {
return block;
}
function toDescendantBlock(block) {
const children = normalizeChildIds(block.children);
return {
...block.block_id ? { block_id: block.block_id } : {},
...children.length > 0 ? { children } : {},
...block
};
}
function normalizeInsertedChildBlocks(children) {
if (!Array.isArray(children)) return [];
return children.filter((child) => typeof child === "object" && child !== null);
}
function normalizeConvertedBlockTree(blocks, firstLevelIds) {
if (blocks.length <= 1) return {
orderedBlocks: blocks,
rootIds: blocks.length === 1 && typeof blocks[0]?.block_id === "string" ? [blocks[0].block_id] : []
};
const byId = /* @__PURE__ */ new Map();
const originalOrder = /* @__PURE__ */ new Map();
for (const [index, block] of blocks.entries()) if (typeof block?.block_id === "string") {
byId.set(block.block_id, block);
originalOrder.set(block.block_id, index);
}
const childIds = /* @__PURE__ */ new Set();
for (const block of blocks) for (const childId of normalizeChildIds(block?.children)) childIds.add(childId);
const inferredTopLevelIds = blocks.filter((block) => {
const blockId = block?.block_id;
if (typeof blockId !== "string") return false;
const parentId = typeof block?.parent_id === "string" ? block.parent_id : "";
return !childIds.has(blockId) && (!parentId || !byId.has(parentId));
}).toSorted((a, b) => (originalOrder.get(a.block_id ?? "__missing__") ?? 0) - (originalOrder.get(b.block_id ?? "__missing__") ?? 0)).map((block) => block.block_id).filter((blockId) => typeof blockId === "string");
const uniqueRootIds = uniqueStrings((firstLevelIds && firstLevelIds.length > 0 ? firstLevelIds : inferredTopLevelIds).filter((id) => typeof id === "string" && byId.has(id)));
const orderedBlocks = [];
const visited = /* @__PURE__ */ new Set();
const visit = (blockId) => {
if (!byId.has(blockId) || visited.has(blockId)) return;
visited.add(blockId);
const block = byId.get(blockId);
if (!block) return;
orderedBlocks.push(block);
for (const childId of normalizeChildIds(block?.children)) visit(childId);
};
for (const rootId of uniqueRootIds) visit(rootId);
for (const block of blocks) if (typeof block?.block_id === "string") visit(block.block_id);
else orderedBlocks.push(block);
return {
orderedBlocks,
rootIds: uniqueRootIds
};
}
async function insertBlocks(client, docToken, blocks, parentBlockId, index) {
const { cleaned, skipped } = cleanBlocksForInsert(blocks);
const blockId = parentBlockId ?? docToken;
if (cleaned.length === 0) return {
children: [],
skipped
};
const allInserted = [];
for (const [offset, block] of cleaned.entries()) {
const res = await client.docx.documentBlockChildren.create({
path: {
document_id: docToken,
block_id: blockId
},
data: {
children: [toCreateChildBlock(block)],
...index !== void 0 ? { index: index + offset } : {}
}
});
if (res.code !== 0) throw new Error(res.msg);
allInserted.push(...res.data?.children ?? []);
}
return {
children: allInserted,
skipped
};
}
/** Split markdown into chunks at top-level headings (# or ##) to stay within API content limits */
function splitMarkdownByHeadings(markdown) {
const lines = markdown.split("\n");
const chunks = [];
let current = [];
let inFencedBlock = false;
for (const line of lines) {
if (/^(`{3,}|~{3,})/.test(line)) inFencedBlock = !inFencedBlock;
if (!inFencedBlock && /^#{1,2}\s/.test(line) && current.length > 0) {
chunks.push(current.join("\n"));
current = [];
}
current.push(line);
}
if (current.length > 0) chunks.push(current.join("\n"));
return chunks;
}
/** Split markdown by size, preferring to break outside fenced code blocks when possible */
function splitMarkdownBySize(markdown, maxChars) {
if (markdown.length <= maxChars) return [markdown];
const lines = markdown.split("\n");
const chunks = [];
let current = [];
let currentLength = 0;
let inFencedBlock = false;
for (const line of lines) {
if (/^(`{3,}|~{3,})/.test(line)) inFencedBlock = !inFencedBlock;
const lineLength = line.length + 1;
const wouldExceed = currentLength + lineLength > maxChars;
if (current.length > 0 && wouldExceed && !inFencedBlock) {
chunks.push(current.join("\n"));
current = [];
currentLength = 0;
}
current.push(line);
currentLength += lineLength;
}
if (current.length > 0) chunks.push(current.join("\n"));
if (chunks.length > 1) return chunks;
const midpoint = Math.floor(lines.length / 2);
if (midpoint <= 0 || midpoint >= lines.length) return [markdown];
return [lines.slice(0, midpoint).join("\n"), lines.slice(midpoint).join("\n")];
}
async function convertMarkdownWithFallback(client, markdown, depth = 0) {
try {
return await convertMarkdown(client, markdown);
} catch (error) {
if (depth >= MAX_CONVERT_RETRY_DEPTH || markdown.length < 2) throw error;
const chunks = splitMarkdownBySize(markdown, Math.max(256, Math.floor(markdown.length / 2)));
if (chunks.length <= 1) throw error;
const blocks = [];
const firstLevelBlockIds = [];
for (const chunk of chunks) {
const converted = await convertMarkdownWithFallback(client, chunk, depth + 1);
blocks.push(...converted.blocks);
firstLevelBlockIds.push(...converted.firstLevelBlockIds);
}
return {
blocks,
firstLevelBlockIds
};
}
}
/** Convert markdown in chunks to avoid document.convert content size limits */
async function chunkedConvertMarkdown(client, markdown) {
const chunks = splitMarkdownByHeadings(markdown);
const allBlocks = [];
const allRootIds = [];
for (const chunk of chunks) {
const { blocks, firstLevelBlockIds } = await convertMarkdownWithFallback(client, chunk);
const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
allBlocks.push(...orderedBlocks);
allRootIds.push(...rootIds);
}
return {
blocks: allBlocks,
firstLevelBlockIds: allRootIds
};
}
/**
* Insert blocks using the Descendant API (supports tables, nested lists, large docs).
* Unlike the Children API, this supports block_type 31/32 (Table/TableCell).
*
* @param parentBlockId - Parent block to insert into (defaults to docToken = document root)
* @param index - Position within parent's children (-1 = end, 0 = first)
*/
async function insertBlocksWithDescendant(client, docToken, blocks, firstLevelBlockIds, { parentBlockId = docToken, index = -1 } = {}) {
const descendants = cleanBlocksForDescendant(blocks);
if (descendants.length === 0) return { children: [] };
const res = await client.docx.documentBlockDescendant.create({
path: {
document_id: docToken,
block_id: parentBlockId
},
data: {
children_id: firstLevelBlockIds,
descendants: descendants.map(toDescendantBlock),
index
}
});
if (res.code !== 0) throw new Error(`${res.msg} (code: ${res.code})`);
return { children: res.data?.children ?? [] };
}
async function clearDocumentContent(client, docToken) {
const existing = await client.docx.documentBlock.list({ path: { document_id: docToken } });
if (existing.code !== 0) throw new Error(existing.msg);
const childIds = existing.data?.items?.filter((b) => b.parent_id === docToken && b.block_type !== 1).map((b) => b.block_id) ?? [];
if (childIds.length > 0) {
const res = await client.docx.documentBlockChildren.batchDelete({
path: {
document_id: docToken,
block_id: docToken
},
data: {
start_index: 0,
end_index: childIds.length
}
});
if (res.code !== 0) throw new Error(res.msg);
}
return childIds.length;
}
async function uploadImageToDocx(client, blockId, imageBuffer, fileName, docToken) {
const fileToken = (await client.drive.media.uploadAll({ data: {
file_name: fileName,
parent_type: "docx_image",
parent_node: blockId,
size: imageBuffer.length,
file: imageBuffer,
...docToken ? { extra: JSON.stringify({ drive_route_token: docToken }) } : {}
} }))?.file_token;
if (!fileToken) throw new Error("Image upload failed: no file_token returned");
return fileToken;
}
async function downloadImage(url, maxBytes) {
return (await getFeishuRuntime().channel.media.readRemoteMediaBuffer({
url,
maxBytes
})).buffer;
}
async function resolveUploadInput(url, filePath, maxBytes, localRoots, explicitFileName, imageInput) {
const inputSources = [
url ? "url" : null,
filePath ? "file_path" : null,
imageInput ? "image" : null
].filter(Boolean);
if (inputSources.length > 1) throw new Error(`Provide only one image source; got: ${inputSources.join(", ")}`);
if (imageInput?.startsWith("data:")) {
const commaIdx = imageInput.indexOf(",");
if (commaIdx === -1) throw new Error("Invalid data URI: missing comma separator.");
const header = imageInput.slice(0, commaIdx);
const data = imageInput.slice(commaIdx + 1);
if (!header.includes(";base64")) throw new Error("Invalid data URI: missing ';base64' marker. Expected format: data:image/png;base64,<base64data>");
const trimmedData = data.trim();
if (trimmedData.length === 0 || !/^[A-Za-z0-9+/]+=*$/.test(trimmedData)) throw new Error(`Invalid data URI: base64 payload contains characters outside the standard alphabet.`);
const ext = extensionForMime(header.match(/data:([^;]+)/)?.[1])?.slice(1) ?? "png";
const estimatedBytes = Math.ceil(trimmedData.length * 3 / 4);
if (estimatedBytes > maxBytes) throw new Error(`Image data URI exceeds limit: estimated ${estimatedBytes} bytes > ${maxBytes} bytes`);
return {
buffer: Buffer.from(trimmedData, "base64"),
fileName: explicitFileName ?? `image.${ext}`
};
}
if (imageInput) {
const candidate = imageInput.startsWith("~") ? imageInput.replace(/^~/, homedir()) : imageInput;
const unambiguousPath = imageInput.startsWith("~") || imageInput.startsWith("./") || imageInput.startsWith("../");
const absolutePath = isAbsolute(imageInput);
if (unambiguousPath || absolutePath && existsSync(candidate)) {
const resolvedPath = resolve(candidate);
return {
buffer: (await getFeishuRuntime().media.loadWebMedia(resolvedPath, {
maxBytes,
optimizeImages: false,
localRoots
})).buffer,
fileName: explicitFileName ?? basename(candidate)
};
}
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,...`);
}
if (imageInput) {
const trimmed = imageInput.trim();
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.");
const estimatedBytes = Math.ceil(trimmed.length * 3 / 4);
if (estimatedBytes > maxBytes) throw new Error(`Base64 image exceeds limit: estimated ${estimatedBytes} bytes > ${maxBytes} bytes`);
const buffer = Buffer.from(trimmed, "base64");
if (buffer.length === 0) throw new Error("Base64 image decoded to empty buffer; check the input.");
return {
buffer,
fileName: explicitFileName ?? "image.png"
};
}
if (!url && !filePath) throw new Error("Either url, file_path, or image (base64/data URI) must be provided");
if (url && filePath) throw new Error("Provide only one of url or file_path");
if (url) {
const fetched = await getFeishuRuntime().channel.media.readRemoteMediaBuffer({
url,
maxBytes
});
const guessed = new URL(url).pathname.split("/").pop() || "upload.bin";
return {
buffer: fetched.buffer,
fileName: explicitFileName || guessed
};
}
const resolvedFilePath = resolve(filePath);
return {
buffer: (await getFeishuRuntime().media.loadWebMedia(resolvedFilePath, {
maxBytes,
optimizeImages: false,
localRoots
})).buffer,
fileName: explicitFileName || basename(filePath)
};
}
async function processImages(client, docToken, markdown, insertedBlocks, maxBytes) {
const imageUrls = extractImageUrls(markdown);
if (imageUrls.length === 0) return 0;
const imageBlocks = insertedBlocks.filter((b) => b.block_type === 27);
let processed = 0;
for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
const url = imageUrls[i];
const blockId = imageBlocks[i]?.block_id;
if (!blockId) continue;
try {
const fileToken = await uploadImageToDocx(client, blockId, await downloadImage(url, maxBytes), new URL(url).pathname.split("/").pop() || `image_${i}.png`, docToken);
await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: blockId
},
data: { replace_image: { token: fileToken } }
});
processed++;
} catch (err) {
console.error(`Failed to process image ${url}:`, err);
}
}
return processed;
}
async function uploadImageBlock(client, docToken, maxBytes, localRoots, url, filePath, parentBlockId, filename, index, imageInput) {
const insertRes = await client.docx.documentBlockChildren.create({
path: {
document_id: docToken,
block_id: parentBlockId ?? docToken
},
params: { document_revision_id: -1 },
data: {
children: [{
block_type: 27,
image: {}
}],
index: index ?? -1
}
});
if (insertRes.code !== 0) throw new Error(`Failed to create image block: ${insertRes.msg}`);
const imageBlockId = insertRes.data?.children?.find((b) => b.block_type === 27)?.block_id;
if (!imageBlockId) throw new Error("Failed to create image block");
const upload = await resolveUploadInput(url, filePath, maxBytes, localRoots, filename, imageInput);
const fileToken = await uploadImageToDocx(client, imageBlockId, upload.buffer, upload.fileName, docToken);
const patchRes = await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: imageBlockId
},
data: { replace_image: { token: fileToken } }
});
if (patchRes.code !== 0) throw new Error(patchRes.msg);
return {
success: true,
block_id: imageBlockId,
file_token: fileToken,
file_name: upload.fileName,
size: upload.buffer.length
};
}
async function uploadFileBlock(client, docToken, maxBytes, localRoots, url, filePath, parentBlockId, filename) {
const blockId = parentBlockId ?? docToken;
const upload = await resolveUploadInput(url, filePath, maxBytes, localRoots, filename);
const converted = await convertMarkdown(client, `[${upload.fileName}](https://example.com/placeholder)`);
const { orderedBlocks } = normalizeConvertedBlockTree(converted.blocks, converted.firstLevelBlockIds);
const { children: inserted } = await insertBlocks(client, docToken, orderedBlocks, blockId);
const placeholderBlock = inserted[0];
if (!placeholderBlock?.block_id) throw new Error("Failed to create placeholder block for file upload");
const parentId = placeholderBlock.parent_id ?? blockId;
const childrenRes = await client.docx.documentBlockChildren.get({ path: {
document_id: docToken,
block_id: parentId
} });
if (childrenRes.code !== 0) throw new Error(childrenRes.msg);
const placeholderIdx = (childrenRes.data?.items ?? []).findIndex((item) => item.block_id === placeholderBlock.block_id);
if (placeholderIdx >= 0) {
const deleteRes = await client.docx.documentBlockChildren.batchDelete({
path: {
document_id: docToken,
block_id: parentId
},
data: {
start_index: placeholderIdx,
end_index: placeholderIdx + 1
}
});
if (deleteRes.code !== 0) throw new Error(deleteRes.msg);
}
const fileToken = (await client.drive.media.uploadAll({ data: {
file_name: upload.fileName,
parent_type: "docx_file",
parent_node: docToken,
size: upload.buffer.length,
file: upload.buffer
} }))?.file_token;
if (!fileToken) throw new Error("File upload failed: no file_token returned");
return {
success: true,
file_token: fileToken,
file_name: upload.fileName,
size: upload.buffer.length,
note: "File uploaded to drive. Use the file_token to reference it. Direct file block creation is not supported by the Feishu API."
};
}
const STRUCTURED_BLOCK_TYPES = new Set([
14,
18,
21,
23,
27,
30,
31,
32
]);
async function readDoc(client, docToken) {
const [contentRes, infoRes, blocksRes] = await Promise.all([
client.docx.document.rawContent({ path: { document_id: docToken } }),
client.docx.document.get({ path: { document_id: docToken } }),
client.docx.documentBlock.list({ path: { document_id: docToken } })
]);
if (contentRes.code !== 0) throw new Error(contentRes.msg);
const blocks = blocksRes.data?.items ?? [];
const blockCounts = {};
const structuredTypes = [];
for (const b of blocks) {
const type = b.block_type ?? 0;
const name = BLOCK_TYPE_NAMES[type] || `type_${type}`;
blockCounts[name] = (blockCounts[name] || 0) + 1;
if (STRUCTURED_BLOCK_TYPES.has(type) && !structuredTypes.includes(name)) structuredTypes.push(name);
}
let hint;
if (structuredTypes.length > 0) 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.`;
return {
title: infoRes.data?.document?.title,
content: contentRes.data?.content,
revision_id: infoRes.data?.document?.revision_id,
block_count: blocks.length,
block_types: blockCounts,
...hint && { hint }
};
}
async function createDoc(client, title, folderToken, options) {
const res = await client.docx.document.create({ data: {
title,
folder_token: folderToken
} });
if (res.code !== 0) throw new Error(res.msg);
const doc = res.data?.document;
const docToken = doc?.document_id;
if (!docToken) throw new Error("Document creation succeeded but no document_id was returned");
const shouldGrantToRequester = options?.grantToRequester !== false;
const requesterOpenId = options?.requesterOpenId?.trim();
const requesterPermType = "edit";
let requesterPermissionAdded = false;
let requesterPermissionSkippedReason;
let requesterPermissionError;
if (shouldGrantToRequester) if (!requesterOpenId) requesterPermissionSkippedReason = "trusted requester identity unavailable";
else try {
await client.drive.permissionMember.create({
path: { token: docToken },
params: {
type: "docx",
need_notification: false
},
data: {
member_type: "openid",
member_id: requesterOpenId,
perm: requesterPermType
}
});
requesterPermissionAdded = true;
} catch (err) {
requesterPermissionError = formatErrorMessage(err);
}
return {
document_id: docToken,
title: doc?.title,
url: `https://feishu.cn/docx/${docToken}`,
...shouldGrantToRequester && {
requester_permission_added: requesterPermissionAdded,
...requesterOpenId && { requester_open_id: requesterOpenId },
requester_perm_type: requesterPermType,
...requesterPermissionSkippedReason && { requester_permission_skipped_reason: requesterPermissionSkippedReason },
...requesterPermissionError && { requester_permission_error: requesterPermissionError }
}
};
}
async function writeDoc(client, docToken, markdown, maxBytes, logger) {
const deleted = await clearDocumentContent(client, docToken);
logger?.info?.("feishu_doc: Converting markdown...");
const { blocks, firstLevelBlockIds } = await chunkedConvertMarkdown(client, markdown);
if (blocks.length === 0) return {
success: true,
blocks_deleted: deleted,
blocks_added: 0,
images_processed: 0
};
logger?.info?.(`feishu_doc: Converted to ${blocks.length} blocks, inserting...`);
const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
const { children: inserted } = blocks.length > 1e3 ? await insertBlocksInBatches(client, docToken, orderedBlocks, rootIds, logger) : await insertBlocksWithDescendant(client, docToken, orderedBlocks, rootIds);
const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
logger?.info?.(`feishu_doc: Done (${blocks.length} blocks, ${imagesProcessed} images)`);
return {
success: true,
blocks_deleted: deleted,
blocks_added: blocks.length,
images_processed: imagesProcessed
};
}
async function appendDoc(client, docToken, markdown, maxBytes, logger) {
logger?.info?.("feishu_doc: Converting markdown...");
const { blocks, firstLevelBlockIds } = await chunkedConvertMarkdown(client, markdown);
if (blocks.length === 0) throw new Error("Content is empty");
logger?.info?.(`feishu_doc: Converted to ${blocks.length} blocks, inserting...`);
const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
const { children: inserted } = blocks.length > 1e3 ? await insertBlocksInBatches(client, docToken, orderedBlocks, rootIds, logger) : await insertBlocksWithDescendant(client, docToken, orderedBlocks, rootIds);
const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
logger?.info?.(`feishu_doc: Done (${blocks.length} blocks, ${imagesProcessed} images)`);
return {
success: true,
blocks_added: blocks.length,
images_processed: imagesProcessed,
block_ids: inserted.map((b) => b.block_id)
};
}
async function insertDoc(client, docToken, markdown, afterBlockId, maxBytes, logger) {
const blockInfo = await client.docx.documentBlock.get({ path: {
document_id: docToken,
block_id: afterBlockId
} });
if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
const parentId = blockInfo.data?.block?.parent_id ?? docToken;
const items = [];
let pageToken;
do {
const childrenRes = await client.docx.documentBlockChildren.get({
path: {
document_id: docToken,
block_id: parentId
},
params: pageToken ? { page_token: pageToken } : {}
});
if (childrenRes.code !== 0) throw new Error(childrenRes.msg);
items.push(...childrenRes.data?.items ?? []);
pageToken = childrenRes.data?.page_token ?? void 0;
} while (pageToken);
const blockIndex = items.findIndex((item) => item.block_id === afterBlockId);
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.`);
const insertIndex = blockIndex + 1;
logger?.info?.("feishu_doc: Converting markdown...");
const { blocks, firstLevelBlockIds } = await chunkedConvertMarkdown(client, markdown);
if (blocks.length === 0) throw new Error("Content is empty");
const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
logger?.info?.(`feishu_doc: Converted to ${blocks.length} blocks, inserting at index ${insertIndex}...`);
const { children: inserted } = blocks.length > 1e3 ? await insertBlocksInBatches(client, docToken, orderedBlocks, rootIds, logger, parentId, insertIndex) : await insertBlocksWithDescendant(client, docToken, orderedBlocks, rootIds, {
parentBlockId: parentId,
index: insertIndex
});
const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
logger?.info?.(`feishu_doc: Done (${blocks.length} blocks, ${imagesProcessed} images)`);
return {
success: true,
blocks_added: blocks.length,
images_processed: imagesProcessed,
block_ids: inserted.map((b) => b.block_id)
};
}
async function createTable(client, docToken, rowSize, columnSize, parentBlockId, columnWidth) {
if (columnWidth && columnWidth.length !== columnSize) throw new Error("column_width length must equal column_size");
const blockId = parentBlockId ?? docToken;
const res = await client.docx.documentBlockChildren.create({
path: {
document_id: docToken,
block_id: blockId
},
data: { children: [{
block_type: 31,
table: { property: {
row_size: rowSize,
column_size: columnSize,
...columnWidth && columnWidth.length > 0 ? { column_width: columnWidth } : {}
} }
}] }
});
if (res.code !== 0) throw new Error(res.msg);
const tableBlock = res.data?.children?.find((b) => b.block_type === 31);
const cells = normalizeInsertedChildBlocks(tableBlock?.children);
return {
success: true,
table_block_id: tableBlock?.block_id,
row_size: rowSize,
column_size: columnSize,
table_cell_block_ids: cells.map((c) => c.block_id).filter(Boolean),
raw_children_count: res.data?.children?.length ?? 0
};
}
async function writeTableCells(client, docToken, tableBlockId, values) {
if (!values.length || !values[0]?.length) throw new Error("values must be a non-empty 2D array");
const tableRes = await client.docx.documentBlock.get({ path: {
document_id: docToken,
block_id: tableBlockId
} });
if (tableRes.code !== 0) throw new Error(tableRes.msg);
const tableBlock = tableRes.data?.block;
if (tableBlock?.block_type !== 31) throw new Error("table_block_id is not a table block");
const tableData = tableBlock.table;
const rows = tableData?.property?.row_size;
const cols = tableData?.property?.column_size;
const cellIds = tableData?.cells ?? [];
if (!rows || !cols || !cellIds.length) throw new Error("Table cell IDs unavailable from table block. Use list_blocks/get_block and pass explicit cell block IDs if needed.");
const writeRows = Math.min(values.length, rows);
let written = 0;
for (let r = 0; r < writeRows; r++) {
const rowValues = values[r] ?? [];
const writeCols = Math.min(rowValues.length, cols);
for (let c = 0; c < writeCols; c++) {
const cellId = cellIds[r * cols + c];
if (!cellId) continue;
const childrenRes = await client.docx.documentBlockChildren.get({ path: {
document_id: docToken,
block_id: cellId
} });
if (childrenRes.code !== 0) throw new Error(childrenRes.msg);
const existingChildren = childrenRes.data?.items ?? [];
if (existingChildren.length > 0) {
const delRes = await client.docx.documentBlockChildren.batchDelete({
path: {
document_id: docToken,
block_id: cellId
},
data: {
start_index: 0,
end_index: existingChildren.length
}
});
if (delRes.code !== 0) throw new Error(delRes.msg);
}
const converted = await convertMarkdown(client, rowValues[c] ?? "");
const { orderedBlocks } = normalizeConvertedBlockTree(converted.blocks, converted.firstLevelBlockIds);
if (orderedBlocks.length > 0) await insertBlocks(client, docToken, orderedBlocks, cellId);
written++;
}
}
return {
success: true,
table_block_id: tableBlockId,
cells_written: written,
table_size: {
rows,
cols
}
};
}
async function createTableWithValues(client, docToken, rowSize, columnSize, values, parentBlockId, columnWidth) {
const tableBlockId = (await createTable(client, docToken, rowSize, columnSize, parentBlockId, columnWidth)).table_block_id;
if (!tableBlockId) throw new Error("create_table succeeded but table_block_id is missing");
return {
success: true,
table_block_id: tableBlockId,
row_size: rowSize,
column_size: columnSize,
cells_written: (await writeTableCells(client, docToken, tableBlockId, values)).cells_written
};
}
async function updateBlock(client, docToken, blockId, content) {
const blockInfo = await client.docx.documentBlock.get({ path: {
document_id: docToken,
block_id: blockId
} });
if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
const res = await client.docx.documentBlock.patch({
path: {
document_id: docToken,
block_id: blockId
},
data: { update_text_elements: { elements: [{ text_run: { content } }] } }
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
block_id: blockId
};
}
async function deleteBlock(client, docToken, blockId) {
const blockInfo = await client.docx.documentBlock.get({ path: {
document_id: docToken,
block_id: blockId
} });
if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
const parentId = blockInfo.data?.block?.parent_id ?? docToken;
const children = await client.docx.documentBlockChildren.get({ path: {
document_id: docToken,
block_id: parentId
} });
if (children.code !== 0) throw new Error(children.msg);
const index = (children.data?.items ?? []).findIndex((item) => item.block_id === blockId);
if (index === -1) throw new Error("Block not found");
const res = await client.docx.documentBlockChildren.batchDelete({
path: {
document_id: docToken,
block_id: parentId
},
data: {
start_index: index,
end_index: index + 1
}
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
deleted_block_id: blockId
};
}
async function listBlocks(client, docToken) {
const res = await client.docx.documentBlock.list({ path: { document_id: docToken } });
if (res.code !== 0) throw new Error(res.msg);
return { blocks: res.data?.items ?? [] };
}
async function getBlock(client, docToken, blockId) {
const res = await client.docx.documentBlock.get({ path: {
document_id: docToken,
block_id: blockId
} });
if (res.code !== 0) throw new Error(res.msg);
return { block: res.data?.block };
}
async function listAppScopes(client) {
const res = await client.application.scope.list({});
if (res.code !== 0) throw new Error(res.msg);
const scopes = res.data?.scopes ?? [];
const granted = scopes.filter((s) => s.grant_status === 1);
const pending = scopes.filter((s) => s.grant_status !== 1);
return {
granted: granted.map((s) => ({
name: s.scope_name,
type: s.scope_type
})),
pending: pending.map((s) => ({
name: s.scope_name,
type: s.scope_type
})),
summary: `${granted.length} granted, ${pending.length} pending`
};
}
function registerFeishuDocTools(api) {
if (!api.config) return;
const accounts = listEnabledFeishuAccounts(api.config);
if (accounts.length === 0) return;
const toolsCfg = resolveAnyEnabledFeishuToolsConfig(accounts);
const registered = [];
const getClient = (params, defaultAccountId) => createFeishuToolClient({
api,
executeParams: params,
defaultAccountId,
requiredTool: {
family: "doc",
label: "Doc"
}
});
const getMediaMaxBytes = (params, defaultAccountId) => (resolveFeishuToolAccount({
api,
executeParams: params,
defaultAccountId,
requiredTool: {
family: "doc",
label: "Doc"
}
}).config?.mediaMaxMb ?? 30) * 1024 * 1024;
if (toolsCfg.doc) {
api.registerTool((ctx) => {
const defaultAccountId = ctx.agentAccountId;
const mediaLocalRoots = resolveDocToolLocalRoots(ctx);
const trustedRequesterOpenId = ctx.messageChannel === "feishu" ? normalizeOptionalString(ctx.requesterSenderId) : void 0;
return {
name: "feishu_doc",
label: "Feishu Doc",
description: "Feishu document operations. Actions: read, write, append, insert, create, list_blocks, get_block, update_block, delete_block, create_table, write_table_cells, create_table_with_values, insert_table_row, insert_table_column, delete_table_rows, delete_table_columns, merge_table_cells, upload_image, upload_file, color_text",
parameters: FeishuDocSchema,
async execute(_toolCallId, params) {
const p = params;
try {
const client = getClient(p, defaultAccountId);
switch (p.action) {
case "read": return json$1(await readDoc(client, p.doc_token));
case "write": return json$1(await writeDoc(client, p.doc_token, p.content, getMediaMaxBytes(p, defaultAccountId), api.logger));
case "append": return json$1(await appendDoc(client, p.doc_token, p.content, getMediaMaxBytes(p, defaultAccountId), api.logger));
case "insert": return json$1(await insertDoc(client, p.doc_token, p.content, p.after_block_id, getMediaMaxBytes(p, defaultAccountId), api.logger));
case "create": return json$1(await createDoc(client, p.title, p.folder_token, {
grantToRequester: p.grant_to_requester,
requesterOpenId: trustedRequesterOpenId
}));
case "list_blocks": return json$1(await listBlocks(client, p.doc_token));
case "get_block": return json$1(await getBlock(client, p.doc_token, p.block_id));
case "update_block": return json$1(await updateBlock(client, p.doc_token, p.block_id, p.content));
case "delete_block": return json$1(await deleteBlock(client, p.doc_token, p.block_id));
case "create_table": return json$1(await createTable(client, p.doc_token, p.row_size, p.column_size, p.parent_block_id, p.column_width));
case "write_table_cells": return json$1(await writeTableCells(client, p.doc_token, p.table_block_id, p.values));
case "create_table_with_values": return json$1(await createTableWithValues(client, p.doc_token, p.row_size, p.column_size, p.values, p.parent_block_id, p.column_width));
case "upload_image": return json$1(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));
case "upload_file": return json$1(await uploadFileBlock(client, p.doc_token, getMediaMaxBytes(p, defaultAccountId), mediaLocalRoots, p.url, p.file_path, p.parent_block_id, p.filename));
case "color_text": return json$1(await updateColorText(client, p.doc_token, p.block_id, p.content));
case "insert_table_row": return json$1(await insertTableRow(client, p.doc_token, p.block_id, p.row_index));
case "insert_table_column": return json$1(await insertTableColumn(client, p.doc_token, p.block_id, p.column_index));
case "delete_table_rows": return json$1(await deleteTableRows(client, p.doc_token, p.block_id, p.row_start, p.row_count));
case "delete_table_columns": return json$1(await deleteTableColumns(client, p.doc_token, p.block_id, p.column_start, p.column_count));
case "merge_table_cells": return json$1(await mergeTableCells(client, p.doc_token, p.block_id, p.row_start, p.row_end, p.column_start, p.column_end));
default: return json$1({ error: "Unknown action" });
}
} catch (err) {
return json$1({ error: formatErrorMessage(err) });
}
}
};
}, { name: "feishu_doc" });
registered.push("feishu_doc");
}
if (toolsCfg.scopes) {
api.registerTool((ctx) => ({
name: "feishu_app_scopes",
label: "Feishu App Scopes",
description: "List current app permissions (scopes). Use to debug permission issues or check available capabilities.",
parameters: Type.Object({}),
async execute() {
try {
return json$1(await listAppScopes(createFeishuToolClient({
api,
defaultAccountId: ctx.agentAccountId,
requiredTool: {
family: "scopes",
label: "App Scopes"
}
})));
} catch (err) {
return json$1({ error: formatErrorMessage(err) });
}
}
}), { name: "feishu_app_scopes" });
registered.push("feishu_app_scopes");
}
}
//#endregion
//#region extensions/feishu/src/wiki-schema.ts
const WIKI_SPACE_ID_DESCRIPTION = "Knowledge space ID. Treat as an opaque string and keep it quoted; never pass numeric-looking IDs as numbers.";
const FeishuWikiSchema = Type.Union([
Type.Object({
action: Type.Literal("spaces"),
page_size: optionalPositiveIntegerSchema({
maximum: 50,
description: "Page size (1-50, default 50)"
}),
page_token: Type.Optional(Type.String({ description: "Pagination token" }))
}),
Type.Object({
action: Type.Literal("nodes"),
space_id: Type.String({ description: WIKI_SPACE_ID_DESCRIPTION }),
parent_node_token: Type.Optional(Type.String({ description: "Parent node token (optional, omit for root)" })),
page_size: optionalPositiveIntegerSchema({
maximum: 50,
description: "Page size (1-50, default 50)"
}),
page_token: Type.Optional(Type.String({ description: "Pagination token" }))
}),
Type.Object({
action: Type.Literal("get"),
token: Type.String({ description: "Wiki node token (from URL /wiki/XXX)" })
}),
Type.Object({
action: Type.Literal("search"),
query: Type.String({ description: "Search query" }),
space_id: Type.Optional(Type.String({ description: "Limit search to this knowledge space. Treat as an opaque string and keep it quoted; never pass numeric-looking IDs as numbers." }))
}),
Type.Object({
action: Type.Literal("create"),
space_id: Type.String({ description: WIKI_SPACE_ID_DESCRIPTION }),
title: Type.String({ description: "Node title" }),
obj_type: Type.Optional(Type.Union([
Type.Literal("docx"),
Type.Literal("sheet"),
Type.Literal("bitable")
], { description: "Object type (default: docx)" })),
parent_node_token: Type.Optional(Type.String({ description: "Parent node token (optional, omit for root)" }))
}),
Type.Object({
action: Type.Literal("move"),
space_id: Type.String({ description: "Source knowledge space ID. Treat as an opaque string and keep it quoted; never pass numeric-looking IDs as numbers." }),
node_token: Type.String({ description: "Node token to move" }),
target_space_id: Type.Optional(Type.String({ description: "Target knowledge space ID (optional, same space if omitted). Treat as an opaque string and keep it quoted; never pass numeric-looking IDs as numbers." })),
target_parent_token: Type.Optional(Type.String({ description: "Target parent node token (optional, root if omitted)" }))
}),
Type.Object({
action: Type.Literal("rename"),
space_id: Type.String({ description: WIKI_SPACE_ID_DESCRIPTION }),
node_token: Type.String({ description: "Node token to rename" }),
title: Type.String({ description: "New title" })
})
]);
//#endregion
//#region extensions/feishu/src/wiki.ts
const WIKI_PAGE_SIZE = 50;
const WIKI_ACCESS_HINT = "To grant wiki access: Open wiki space → Settings → Members → Add the bot. See: https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa#a40ad4ca";
function requireWikiSpaceId(value, fieldName) {
if (typeof value !== "string") throw new Error(`${fieldName} must be a string. Feishu wiki space IDs are opaque identifiers; pass them quoted to avoid JavaScript number precision loss.`);
const trimmed = value.trim();
if (!trimmed) throw new Error(`${fieldName} must not be empty.`);
return trimmed;
}
function optionalWikiSpaceId(value, fieldName) {
if (value === void 0 || value === null || value === "") return;
return requireWikiSpaceId(value, fieldName);
}
function readWikiPageSize(params) {
return readPositiveIntegerParam(params, "page_size", {
max: WIKI_PAGE_SIZE,
message: "page_size must be a positive integer between 1 and 50"
}) ?? WIKI_PAGE_SIZE;
}
async function listSpaces(client, pageSize, pageToken) {
const res = await client.wiki.space.list({ params: {
page_size: pageSize,
page_token: pageToken
} });
if (res.code !== 0) throw new Error(res.msg);
const spaces = res.data?.items?.map((s) => ({
space_id: s.space_id,
name: s.name,
description: s.description,
visibility: s.visibility
})) ?? [];
return {
spaces,
has_more: res.data?.has_more ?? false,
page_token: res.data?.page_token,
...spaces.length === 0 && pageToken === void 0 && res.data?.has_more !== true && { hint: WIKI_ACCESS_HINT }
};
}
async function listNodes(client, spaceId, parentNodeToken, pageSize, pageToken) {
const res = await client.wiki.spaceNode.list({
path: { space_id: spaceId },
params: {
parent_node_token: parentNodeToken,
page_size: pageSize,
page_token: pageToken
}
});
if (res.code !== 0) throw new Error(res.msg);
return {
nodes: res.data?.items?.map((n) => ({
node_token: n.node_token,
obj_token: n.obj_token,
obj_type: n.obj_type,
title: n.title,
has_child: n.has_child
})) ?? [],
has_more: res.data?.has_more ?? false,
page_token: res.data?.page_token
};
}
async function getNode(client, token) {
const res = await client.wiki.space.getNode({ params: { token } });
if (res.code !== 0) throw new Error(res.msg);
const node = res.data?.node;
return {
node_token: node?.node_token,
space_id: node?.space_id,
obj_token: node?.obj_token,
obj_type: node?.obj_type,
title: node?.title,
parent_node_token: node?.parent_node_token,
has_child: node?.has_child,
creator: node?.creator,
create_time: node?.node_create_time
};
}
async function createNode(client, spaceId, title, objType, parentNodeToken) {
const res = await client.wiki.spaceNode.create({
path: { space_id: spaceId },
data: {
obj_type: objType || "docx",
node_type: "origin",
title,
parent_node_token: parentNodeToken
}
});
if (res.code !== 0) throw new Error(res.msg);
const node = res.data?.node;
return {
node_token: node?.node_token,
obj_token: node?.obj_token,
obj_type: node?.obj_type,
title: node?.title
};
}
async function moveNode(client, spaceId, nodeToken, targetSpaceId, targetParentToken) {
const res = await client.wiki.spaceNode.move({
path: {
space_id: spaceId,
node_token: nodeToken
},
data: {
target_space_id: targetSpaceId || spaceId,
target_parent_token: targetParentToken
}
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
node_token: res.data?.node?.node_token
};
}
async function renameNode(client, spaceId, nodeToken, title) {
const res = await client.wiki.spaceNode.updateTitle({
path: {
space_id: spaceId,
node_token: nodeToken
},
data: { title }
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
node_token: nodeToken,
title
};
}
function registerFeishuWikiTools(api) {
if (!api.config) return;
const accounts = listEnabledFeishuAccounts(api.config);
if (accounts.length === 0) return;
if (!resolveAnyEnabledFeishuToolsConfig(accounts).wiki) return;
api.registerTool((ctx) => {
const defaultAccountId = ctx.agentAccountId;
return {
name: "feishu_wiki",
label: "Feishu Wiki",
description: "Feishu knowledge base operations. Actions: spaces, nodes, get, create, move, rename",
parameters: FeishuWikiSchema,
async execute(_toolCallId, params) {
const p = params;
try {
const createClient = () => createFeishuToolClient({
api,
executeParams: p,
defaultAccountId,
requiredTool: {
family: "wiki",
label: "Wiki"
}
});
switch (p.action) {
case "spaces": return jsonToolResult(await listSpaces(createClient(), readWikiPageSize(p), p.page_token));
case "nodes": {
const spaceId = requireWikiSpaceId(p.space_id, "space_id");
return jsonToolResult(await listNodes(createClient(), spaceId, p.parent_node_token, readWikiPageSize(p), p.page_token));
}
case "get": return jsonToolResult(await getNode(createClient(), p.token));
case "search":
optionalWikiSpaceId(p.space_id, "space_id");
createClient();
return jsonToolResult({ error: "Search is not available. Use feishu_wiki with action: 'nodes' to browse or action: 'get' to lookup by token." });
case "create": {
const spaceId = requireWikiSpaceId(p.space_id, "space_id");
return jsonToolResult(await createNode(createClient(), spaceId, p.title, p.obj_type, p.parent_node_token));
}
case "move": {
const spaceId = requireWikiSpaceId(p.space_id, "space_id");
return jsonToolResult(await moveNode(createClient(), spaceId, p.node_token, optionalWikiSpaceId(p.target_space_id, "target_space_id"), p.target_parent_token));
}
case "rename": {
const spaceId = requireWikiSpaceId(p.space_id, "space_id");
return jsonToolResult(await renameNode(createClient(), spaceId, p.node_token, p.title));
}
default: return unknownToolActionResult(p.action);
}
} catch (err) {
return toolExecutionErrorResult(err);
}
}
};
}, { name: "feishu_wiki" });
}
//#endregion
//#region extensions/feishu/src/perm-schema.ts
const TokenType = Type.Union([
Type.Literal("doc"),
Type.Literal("docx"),
Type.Literal("sheet"),
Type.Literal("bitable"),
Type.Literal("folder"),
Type.Literal("file"),
Type.Literal("wiki"),
Type.Literal("mindnote")
]);
const MemberType = Type.Union([
Type.Literal("email"),
Type.Literal("openid"),
Type.Literal("userid"),
Type.Literal("unionid"),
Type.Literal("openchat"),
Type.Literal("opendepartmentid")
]);
const Permission = Type.Union([
Type.Literal("view"),
Type.Literal("edit"),
Type.Literal("full_access")
]);
const FeishuPermSchema = Type.Union([
Type.Object({
action: Type.Literal("list"),
token: Type.String({ description: "File token" }),
type: TokenType
}),
Type.Object({
action: Type.Literal("add"),
token: Type.String({ description: "File token" }),
type: TokenType,
member_type: MemberType,
member_id: Type.String({ description: "Member ID (email, open_id, user_id, etc.)" }),
perm: Permission
}),
Type.Object({
action: Type.Literal("remove"),
token: Type.String({ description: "File token" }),
type: TokenType,
member_type: MemberType,
member_id: Type.String({ description: "Member ID to remove" })
})
]);
//#endregion
//#region extensions/feishu/src/perm.ts
async function listMembers(client, token, type) {
const res = await client.drive.permissionMember.list({
path: { token },
params: { type }
});
if (res.code !== 0) throw new Error(res.msg);
return { members: res.data?.items?.map((m) => ({
member_type: m.member_type,
member_id: m.member_id,
perm: m.perm,
name: m.name
})) ?? [] };
}
async function addMember(client, token, type, memberType, memberId, perm) {
const res = await client.drive.permissionMember.create({
path: { token },
params: {
type,
need_notification: false
},
data: {
member_type: memberType,
member_id: memberId,
perm
}
});
if (res.code !== 0) throw new Error(res.msg);
return {
success: true,
member: res.data?.member
};
}
async function removeMember(client, token, type, memberType, memberId) {
const res = await client.drive.permissionMember.delete({
path: {
token,
member_id: memberId
},
params: {
type,
member_type: memberType
}
});
if (res.code !== 0) throw new Error(res.msg);
return { success: true };
}
function registerFeishuPermTools(api) {
if (!api.config) return;
const accounts = listEnabledFeishuAccounts(api.config);
if (accounts.length === 0) return;
if (!resolveAnyEnabledFeishuToolsConfig(accounts).perm) return;
api.registerTool((ctx) => {
const defaultAccountId = ctx.agentAccountId;
return {
name: "feishu_perm",
label: "Feishu Perm",
description: "Feishu permission management. Actions: list, add, remove",
parameters: FeishuPermSchema,
async execute(_toolCallId, params) {
const p = params;
try {
const client = createFeishuToolClient({
api,
executeParams: p,
defaultAccountId,
requiredTool: {
family: "perm",
label: "Perm"
}
});
switch (p.action) {
case "list": return jsonToolResult(await listMembers(client, p.token, p.type));
case "add": return jsonToolResult(await addMember(client, p.token, p.type, p.member_type, p.member_id, p.perm));
case "remove": return jsonToolResult(await removeMember(client, p.token, p.type, p.member_type, p.member_id));
default: return unknownToolActionResult(p.action);
}
} catch (err) {
return toolExecutionErrorResult(err);
}
}
};
}, { name: "feishu_perm" });
}
//#endregion
//#region extensions/feishu/src/bitable.ts
function json(data) {
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}],
details: data
};
}
var LarkApiError = class extends Error {
constructor(code, message, api, context) {
super(`[${api}] code=${code} message=${message}`);
this.name = "LarkApiError";
this.code = code;
this.api = api;
this.context = context;
}
};
function ensureLarkSuccess(res, api, context) {
if (res.code !== 0) throw new LarkApiError(res.code ?? -1, res.msg ?? "unknown error", api, context);
}
/** Field type ID to human-readable name */
const FIELD_TYPE_NAMES = {
1: "Text",
2: "Number",
3: "SingleSelect",
4: "MultiSelect",
5: "DateTime",
7: "Checkbox",
11: "User",
13: "Phone",
15: "URL",
17: "Attachment",
18: "SingleLink",
19: "Lookup",
20: "Formula",
21: "DuplexLink",
22: "Location",
23: "GroupChat",
1001: "CreatedTime",
1002: "ModifiedTime",
1003: "CreatedUser",
1004: "ModifiedUser",
1005: "AutoNumber"
};
/** Parse bitable URL and extract tokens */
function parseBitableUrl(url) {
try {
const u = new URL(url);
const tableId = u.searchParams.get("table") ?? void 0;
const wikiMatch = u.pathname.match(/\/wiki\/([A-Za-z0-9]+)/);
if (wikiMatch) return {
token: wikiMatch[1],
tableId,
isWiki: true
};
const baseMatch = u.pathname.match(/\/base\/([A-Za-z0-9]+)/);
if (baseMatch) return {
token: baseMatch[1],
tableId,
isWiki: false
};
return null;
} catch {
return null;
}
}
/** Get app_token from wiki node_token */
async function getAppTokenFromWiki(client, nodeToken) {
const res = await client.wiki.space.getNode({ params: { token: nodeToken } });
ensureLarkSuccess(res, "wiki.space.getNode", { nodeToken });
const node = res.data?.node;
if (!node) throw new Error("Node not found");
if (node.obj_type !== "bitable") throw new Error(`Node is not a bitable (type: ${node.obj_type})`);
return node.obj_token;
}
/** Get bitable metadata from URL (handles both /base/ and /wiki/ URLs) */
async function getBitableMeta(client, url) {
const parsed = parseBitableUrl(url);
if (!parsed) throw new Error("Invalid URL format. Expected /base/XXX or /wiki/XXX URL");
let appToken;
if (parsed.isWiki) appToken = await getAppTokenFromWiki(client, parsed.token);
else appToken = parsed.token;
const res = await client.bitable.app.get({ path: { app_token: appToken } });
ensureLarkSuccess(res, "bitable.app.get", { appToken });
let tables = [];
if (!parsed.tableId) {
const tablesRes = await client.bitable.appTable.list({ path: { app_token: appToken } });
if (tablesRes.code === 0) tables = (tablesRes.data?.items ?? []).map((t) => ({
table_id: t.table_id,
name: t.name
}));
}
return {
app_token: appToken,
table_id: parsed.tableId,
name: res.data?.app?.name,
url_type: parsed.isWiki ? "wiki" : "base",
...tables.length > 0 && { tables },
hint: parsed.tableId ? `Use app_token="${appToken}" and table_id="${parsed.tableId}" for other bitable tools` : `Use app_token="${appToken}" for other bitable tools. Select a table_id from the tables list.`
};
}
async function listFields(client, appToken, tableId) {
const res = await client.bitable.appTableField.list({ path: {
app_token: appToken,
table_id: tableId
} });
ensureLarkSuccess(res, "bitable.appTableField.list", {
appToken,
tableId
});
const fields = res.data?.items ?? [];
return {
fields: fields.map((f) => ({
field_id: f.field_id,
field_name: f.field_name,
type: f.type,
type_name: FIELD_TYPE_NAMES[f.type ?? 0] || `type_${f.type}`,
is_primary: f.is_primary,
...f.property && { property: f.property }
})),
total: fields.length
};
}
async function listRecords(client, appToken, tableId, pageSize, pageToken) {
const res = await client.bitable.appTableRecord.list({
path: {
app_token: appToken,
table_id: tableId
},
params: {
page_size: pageSize ?? 100,
...pageToken && { page_token: pageToken }
}
});
ensureLarkSuccess(res, "bitable.appTableRecord.list", {
appToken,
tableId,
pageSize
});
return {
records: res.data?.items ?? [],
has_more: res.data?.has_more ?? false,
page_token: res.data?.page_token,
total: res.data?.total
};
}
function readBitableListRecordsPageSize(params) {
return readPositiveIntegerParam(params, "page_size", {
max: 500,
message: "page_size must be a positive integer between 1 and 500"
});
}
async function getRecord(client, appToken, tableId, recordId) {
const res = await client.bitable.appTableRecord.get({ path: {
app_token: appToken,
table_id: tableId,
record_id: recordId
} });
ensureLarkSuccess(res, "bitable.appTableRecord.get", {
appToken,
tableId,
recordId
});
return { record: res.data?.record };
}
async function createRecord(client, appToken, tableId, fields) {
const res = await client.bitable.appTableRecord.create({
path: {
app_token: appToken,
table_id: tableId
},
data: { fields }
});
ensureLarkSuccess(res, "bitable.appTableRecord.create", {
appToken,
tableId
});
return { record: res.data?.record };
}
/** Default field types created for new Bitable tables (to be cleaned up) */
const DEFAULT_CLEANUP_FIELD_TYPES = new Set([
3,
5,
17
]);
function isDefaultEmptyBitableFieldValue(value) {
if (value === void 0 || value === null || value === "") return true;
if (Array.isArray(value)) return value.every(isDefaultEmptyBitableFieldValue);
if (typeof value === "object") {
const record = value;
const keys = Object.keys(record);
if (keys.length === 0) return true;
if ("text" in record && keys.every((key) => key === "text" || key === "type")) return record.text === void 0 || record.text === null || record.text === "";
return Object.values(record).every(isDefaultEmptyBitableFieldValue);
}
return false;
}
function isPlaceholderBitableRecord(fields) {
if (!fields || typeof fields !== "object" || Array.isArray(fields)) return true;
return Object.values(fields).every(isDefaultEmptyBitableFieldValue);
}
/** Clean up default placeholder rows and fields in a newly created Bitable table */
async function cleanupNewBitable(client, appToken, tableId, tableName, logger) {
let cleanedRows = 0;
let cleanedFields = 0;
const fieldsRes = await client.bitable.appTableField.list({ path: {
app_token: appToken,
table_id: tableId
} });
if (fieldsRes.code === 0 && fieldsRes.data?.items) {
const primaryField = fieldsRes.data.items.find((f) => f.is_primary);
if (primaryField?.field_id) try {
const newFieldName = tableName.length <= 20 ? tableName : "Name";
await client.bitable.appTableField.update({
path: {
app_token: appToken,
table_id: tableId,
field_id: primaryField.field_id
},
data: {
field_name: newFieldName,
type: 1
}
});
cleanedFields++;
} catch (err) {
logger.debug(`Failed to rename primary field: ${String(err)}`);
}
const defaultFieldsToDelete = fieldsRes.data.items.filter((f) => !f.is_primary && DEFAULT_CLEANUP_FIELD_TYPES.has(f.type ?? 0));
for (const field of defaultFieldsToDelete) if (field.field_id) try {
await client.bitable.appTableField.delete({ path: {
app_token: appToken,
table_id: tableId,
field_id: field.field_id
} });
cleanedFields++;
} catch (err) {
logger.debug(`Failed to delete default field ${field.field_name}: ${String(err)}`);
}
}
const recordsRes = await client.bitable.appTableRecord.list({
path: {
app_token: appToken,
table_id: tableId
},
params: { page_size: 100 }
});
if (recordsRes.code === 0 && recordsRes.data?.items) {
const emptyRecordIds = recordsRes.data.items.filter((r) => isPlaceholderBitableRecord(r.fields)).map((r) => r.record_id).filter((id) => Boolean(id));
if (emptyRecordIds.length > 0) try {
await client.bitable.appTableRecord.batchDelete({
path: {
app_token: appToken,
table_id: tableId
},
data: { records: emptyRecordIds }
});
cleanedRows = emptyRecordIds.length;
} catch {
for (const recordId of emptyRecordIds) try {
await client.bitable.appTableRecord.delete({ path: {
app_token: appToken,
table_id: tableId,
record_id: recordId
} });
cleanedRows++;
} catch (err) {
logger.debug(`Failed to delete empty row ${recordId}: ${String(err)}`);
}
}
}
return {
cleanedRows,
cleanedFields
};
}
async function createApp(client, name, folderToken, logger) {
const res = await client.bitable.app.create({ data: {
name,
...folderToken && { folder_token: folderToken }
} });
ensureLarkSuccess(res, "bitable.app.create", {
name,
folderToken
});
const appToken = res.data?.app?.app_token;
if (!appToken) throw new Error("Failed to create Bitable: no app_token returned");
const log = logger ?? {
debug: () => {},
warn: () => {}
};
let tableId;
let cleanedRows = 0;
let cleanedFields = 0;
try {
const tablesRes = await client.bitable.appTable.list({ path: { app_token: appToken } });
if (tablesRes.code === 0 && tablesRes.data?.items && tablesRes.data.items.length > 0) {
tableId = tablesRes.data.items[0].table_id ?? void 0;
if (tableId) {
const cleanup = await cleanupNewBitable(client, appToken, tableId, name, log);
cleanedRows = cleanup.cleanedRows;
cleanedFields = cleanup.cleanedFields;
}
}
} catch (err) {
log.debug(`Cleanup failed (non-critical): ${String(err)}`);
}
return {
app_token: appToken,
table_id: tableId,
name: res.data?.app?.name,
url: res.data?.app?.url,
cleaned_placeholder_rows: cleanedRows,
cleaned_default_fields: cleanedFields,
hint: tableId ? `Table created. Use app_token="${appToken}" and table_id="${tableId}" for other bitable tools.` : "Table created. Use feishu_bitable_get_meta to get table_id and field details."
};
}
async function createField(client, appToken, tableId, fieldName, fieldType, property) {
const res = await client.bitable.appTableField.create({
path: {
app_token: appToken,
table_id: tableId
},
data: {
field_name: fieldName,
type: fieldType,
...property && { property }
}
});
ensureLarkSuccess(res, "bitable.appTableField.create", {
appToken,
tableId,
fieldName,
fieldType
});
return {
field_id: res.data?.field?.field_id,
field_name: res.data?.field?.field_name,
type: res.data?.field?.type,
type_name: FIELD_TYPE_NAMES[res.data?.field?.type ?? 0] || `type_${res.data?.field?.type}`
};
}
async function updateRecord(client, appToken, tableId, recordId, fields) {
const res = await client.bitable.appTableRecord.update({
path: {
app_token: appToken,
table_id: tableId,
record_id: recordId
},
data: { fields }
});
ensureLarkSuccess(res, "bitable.appTableRecord.update", {
appToken,
tableId,
recordId
});
return { record: res.data?.record };
}
const GetMetaSchema = Type.Object({ url: Type.String({ description: "Bitable URL. Supports both formats: /base/XXX?table=YYY or /wiki/XXX?table=YYY" }) });
const ListFieldsSchema = Type.Object({
app_token: Type.String({ description: "Bitable app token (use feishu_bitable_get_meta to get from URL)" }),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" })
});
const ListRecordsSchema = Type.Object({
app_token: Type.String({ description: "Bitable app token (use feishu_bitable_get_meta to get from URL)" }),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
page_size: optionalPositiveIntegerSchema({
description: "Number of records per page (1-500, default 100)",
maximum: 500
}),
page_token: Type.Optional(Type.String({ description: "Pagination token from previous response" }))
});
const GetRecordSchema = Type.Object({
app_token: Type.String({ description: "Bitable app token (use feishu_bitable_get_meta to get from URL)" }),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
record_id: Type.String({ description: "Record ID to retrieve" })
});
const CreateRecordSchema = Type.Object({
app_token: Type.String({ description: "Bitable app token (use feishu_bitable_get_meta to get from URL)" }),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
fields: Type.Record(Type.String(), Type.Any(), { description: "Field values keyed by field name. Format by type: Text='string', Number=123, SingleSelect='Option', MultiSelect=['A','B'], DateTime=timestamp_ms, User=[{id:'ou_xxx'}], URL={text:'Display',link:'https://...'}" })
});
const CreateAppSchema = Type.Object({
name: Type.String({ description: "Name for the new Bitable application" }),
folder_token: Type.Optional(Type.String({ description: "Optional folder token to place the Bitable in a specific folder" }))
});
const CreateFieldSchema = Type.Object({
app_token: Type.String({ description: "Bitable app token (use feishu_bitable_get_meta to get from URL, or feishu_bitable_create_app to create new)" }),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
field_name: Type.String({ description: "Name for the new field" }),
field_type: Type.Number({
description: "Field type ID: 1=Text, 2=Number, 3=SingleSelect, 4=MultiSelect, 5=DateTime, 7=Checkbox, 11=User, 13=Phone, 15=URL, 17=Attachment, 18=SingleLink, 19=Lookup, 20=Formula, 21=DuplexLink, 22=Location, 23=GroupChat, 1001=CreatedTime, 1002=ModifiedTime, 1003=CreatedUser, 1004=ModifiedUser, 1005=AutoNumber",
minimum: 1
}),
property: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Field-specific properties (e.g., options for SingleSelect, format for Number)" }))
});
const UpdateRecordSchema = Type.Object({
app_token: Type.String({ description: "Bitable app token (use feishu_bitable_get_meta to get from URL)" }),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
record_id: Type.String({ description: "Record ID to update" }),
fields: Type.Record(Type.String(), Type.Any(), { description: "Field values to update (same format as create_record)" })
});
function registerFeishuBitableTools(api) {
if (!api.config) return;
const accounts = listEnabledFeishuAccounts(api.config);
if (accounts.length === 0) return;
if (!resolveAnyEnabledFeishuToolsConfig(accounts).bitable) return;
const getClient = (params, defaultAccountId) => {
const account = resolveFeishuToolAccount({
api,
executeParams: params,
defaultAccountId
});
if (!resolveToolsConfig(account.config.tools).bitable) throw new Error(`Feishu Bitable tools are disabled for account "${account.accountId}"`);
return createFeishuClient(account);
};
const registerBitableTool = (params) => {
api.registerTool((ctx) => ({
name: params.name,
label: params.label,
description: params.description,
parameters: params.parameters,
async execute(_toolCallId, rawParams) {
try {
return json(await params.execute({
params: rawParams,
defaultAccountId: ctx.agentAccountId
}));
} catch (err) {
return json({ error: formatErrorMessage(err) });
}
}
}), { name: params.name });
};
registerBitableTool({
name: "feishu_bitable_get_meta",
label: "Feishu Bitable Get Meta",
description: "Parse a Bitable URL and get app_token, table_id, and table list. Use this first when given a /wiki/ or /base/ URL.",
parameters: GetMetaSchema,
async execute({ params, defaultAccountId }) {
return getBitableMeta(getClient(params, defaultAccountId), params.url);
}
});
registerBitableTool({
name: "feishu_bitable_list_fields",
label: "Feishu Bitable List Fields",
description: "List all fields (columns) in a Bitable table with their types and properties",
parameters: ListFieldsSchema,
async execute({ params, defaultAccountId }) {
return listFields(getClient(params, defaultAccountId), params.app_token, params.table_id);
}
});
registerBitableTool({
name: "feishu_bitable_list_records",
label: "Feishu Bitable List Records",
description: "List records (rows) from a Bitable table with pagination support",
parameters: ListRecordsSchema,
async execute({ params, defaultAccountId }) {
return listRecords(getClient(params, defaultAccountId), params.app_token, params.table_id, readBitableListRecordsPageSize(params), params.page_token);
}
});
registerBitableTool({
name: "feishu_bitable_get_record",
label: "Feishu Bitable Get Record",
description: "Get a single record by ID from a Bitable table",
parameters: GetRecordSchema,
async execute({ params, defaultAccountId }) {
return getRecord(getClient(params, defaultAccountId), params.app_token, params.table_id, params.record_id);
}
});
registerBitableTool({
name: "feishu_bitable_create_record",
label: "Feishu Bitable Create Record",
description: "Create a new record (row) in a Bitable table",
parameters: CreateRecordSchema,
async execute({ params, defaultAccountId }) {
return createRecord(getClient(params, defaultAccountId), params.app_token, params.table_id, params.fields);
}
});
registerBitableTool({
name: "feishu_bitable_update_record",
label: "Feishu Bitable Update Record",
description: "Update an existing record (row) in a Bitable table",
parameters: UpdateRecordSchema,
async execute({ params, defaultAccountId }) {
return updateRecord(getClient(params, defaultAccountId), params.app_token, params.table_id, params.record_id, params.fields);
}
});
registerBitableTool({
name: "feishu_bitable_create_app",
label: "Feishu Bitable Create App",
description: "Create a new Bitable (multidimensional table) application",
parameters: CreateAppSchema,
async execute({ params, defaultAccountId }) {
return createApp(getClient(params, defaultAccountId), params.name, params.folder_token, {
debug: (msg) => api.logger.debug?.(msg),
warn: (msg) => api.logger.warn?.(msg)
});
}
});
registerBitableTool({
name: "feishu_bitable_create_field",
label: "Feishu Bitable Create Field",
description: "Create a new field (column) in a Bitable table",
parameters: CreateFieldSchema,
async execute({ params, defaultAccountId }) {
return createField(getClient(params, defaultAccountId), params.app_token, params.table_id, params.field_name, params.field_type, params.property);
}
});
}
//#endregion
//#region extensions/feishu/api.ts
const feishuSessionBindingAdapterChannels = ["feishu"];
//#endregion
export { testing as __testing, testing as feishuThreadBindingTesting, testing, buildFeishuConversationId, buildFeishuModelOverrideParentCandidates, createClackPrompter, createFeishuThreadBindingManager, feishuPlugin, feishuSessionBindingAdapterChannels, feishuSetupAdapter, feishuSetupWizard, getFeishuThreadBindingManager, handleFeishuSubagentDeliveryTarget, handleFeishuSubagentEnded, handleFeishuSubagentSpawning, parseFeishuConversationId, parseFeishuDirectConversationId, parseFeishuTargetId, registerFeishuBitableTools, registerFeishuChatTools, registerFeishuDocTools, registerFeishuDriveTools, registerFeishuPermTools, registerFeishuWikiTools, runFeishuLogin, setFeishuNamedAccountEnabled };