@xwang152/claw-lark 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -3
- package/README_zh.md +45 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/src/channel.js +4 -21
- package/dist/src/channel.js.map +1 -1
- package/dist/src/directory.d.ts +38 -2
- package/dist/src/directory.js +49 -8
- package/dist/src/directory.js.map +1 -1
- package/dist/src/docx.d.ts +169 -0
- package/dist/src/docx.js +517 -0
- package/dist/src/docx.js.map +1 -0
- package/dist/src/media.js +87 -46
- package/dist/src/media.js.map +1 -1
- package/dist/src/mention.d.ts +16 -1
- package/dist/src/mention.js +22 -2
- package/dist/src/mention.js.map +1 -1
- package/dist/src/message-context.js +6 -3
- package/dist/src/message-context.js.map +1 -1
- package/dist/src/monitor.js +26 -38
- package/dist/src/monitor.js.map +1 -1
- package/dist/src/probe.js +9 -6
- package/dist/src/probe.js.map +1 -1
- package/dist/src/provider.js +1 -19
- package/dist/src/provider.js.map +1 -1
- package/dist/src/reactions.js.map +1 -1
- package/dist/src/send.js +3 -4
- package/dist/src/send.js.map +1 -1
- package/dist/src/sender-name.d.ts +1 -0
- package/dist/src/sender-name.js +48 -1
- package/dist/src/sender-name.js.map +1 -1
- package/dist/src/typing.js +39 -44
- package/dist/src/typing.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lark/Feishu Document Tools
|
|
3
|
+
*
|
|
4
|
+
* Functions for creating, reading, updating, and managing Lark documents.
|
|
5
|
+
* Supports markdown-to-document conversion with image handling.
|
|
6
|
+
*/
|
|
7
|
+
import type { ResolvedLarkAccount } from "./types.js";
|
|
8
|
+
export type LarkBlockType = number;
|
|
9
|
+
export type LarkDocumentInfo = {
|
|
10
|
+
title?: string;
|
|
11
|
+
content?: string;
|
|
12
|
+
revision_id?: string;
|
|
13
|
+
block_count: number;
|
|
14
|
+
block_types: Record<string, number>;
|
|
15
|
+
hint?: string;
|
|
16
|
+
};
|
|
17
|
+
export type LarkCreateDocResult = {
|
|
18
|
+
document_id?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
url: string;
|
|
21
|
+
};
|
|
22
|
+
export type LarkWriteDocResult = {
|
|
23
|
+
success: boolean;
|
|
24
|
+
blocks_deleted: number;
|
|
25
|
+
blocks_added: number;
|
|
26
|
+
images_processed: number;
|
|
27
|
+
images_failed: number;
|
|
28
|
+
image_errors: Array<{
|
|
29
|
+
url: string;
|
|
30
|
+
error: string;
|
|
31
|
+
}>;
|
|
32
|
+
warning?: string;
|
|
33
|
+
};
|
|
34
|
+
export type LarkAppendDocResult = {
|
|
35
|
+
success: boolean;
|
|
36
|
+
blocks_added: number;
|
|
37
|
+
images_processed: number;
|
|
38
|
+
images_failed: number;
|
|
39
|
+
image_errors: Array<{
|
|
40
|
+
url: string;
|
|
41
|
+
error: string;
|
|
42
|
+
}>;
|
|
43
|
+
block_ids: string[];
|
|
44
|
+
warning?: string;
|
|
45
|
+
};
|
|
46
|
+
export type LarkBlock = {
|
|
47
|
+
block_id?: string;
|
|
48
|
+
block_type: number;
|
|
49
|
+
parent_id?: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
export type LarkListBlocksResult = {
|
|
53
|
+
blocks: LarkBlock[];
|
|
54
|
+
};
|
|
55
|
+
export type LarkGetBlockResult = {
|
|
56
|
+
block: LarkBlock | undefined;
|
|
57
|
+
};
|
|
58
|
+
export type LarkFolderItem = {
|
|
59
|
+
token: string;
|
|
60
|
+
name: string;
|
|
61
|
+
type: string;
|
|
62
|
+
url: string;
|
|
63
|
+
};
|
|
64
|
+
export type LarkListFolderResult = {
|
|
65
|
+
files: LarkFolderItem[];
|
|
66
|
+
};
|
|
67
|
+
export type LarkAppScopeResult = {
|
|
68
|
+
granted: Array<{
|
|
69
|
+
name: string;
|
|
70
|
+
type: string;
|
|
71
|
+
}>;
|
|
72
|
+
pending: Array<{
|
|
73
|
+
name: string;
|
|
74
|
+
type: string;
|
|
75
|
+
}>;
|
|
76
|
+
summary: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Read a Lark document's content and metadata.
|
|
80
|
+
*
|
|
81
|
+
* @param account - The Lark account to use
|
|
82
|
+
* @param docToken - The document token (from URL /docx/XXX)
|
|
83
|
+
* @returns Document info with title, content, block counts
|
|
84
|
+
*/
|
|
85
|
+
export declare function readDocumentLark(account: ResolvedLarkAccount, docToken: string): Promise<LarkDocumentInfo>;
|
|
86
|
+
/**
|
|
87
|
+
* Create a new Lark document.
|
|
88
|
+
*
|
|
89
|
+
* @param account - The Lark account to use
|
|
90
|
+
* @param title - Document title
|
|
91
|
+
* @param folderToken - Optional parent folder token
|
|
92
|
+
* @returns Created document info with ID and URL
|
|
93
|
+
*/
|
|
94
|
+
export declare function createDocumentLark(account: ResolvedLarkAccount, title: string, folderToken?: string): Promise<LarkCreateDocResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Write markdown content to a Lark document (replaces all content).
|
|
97
|
+
*
|
|
98
|
+
* @param account - The Lark account to use
|
|
99
|
+
* @param docToken - Document token
|
|
100
|
+
* @param markdown - Markdown content to write
|
|
101
|
+
* @returns Write result with block counts and warnings
|
|
102
|
+
*/
|
|
103
|
+
export declare function writeDocumentLark(account: ResolvedLarkAccount, docToken: string, markdown: string): Promise<LarkWriteDocResult>;
|
|
104
|
+
/**
|
|
105
|
+
* Append markdown content to a Lark document.
|
|
106
|
+
*
|
|
107
|
+
* @param account - The Lark account to use
|
|
108
|
+
* @param docToken - Document token
|
|
109
|
+
* @param markdown - Markdown content to append
|
|
110
|
+
* @returns Append result with block counts
|
|
111
|
+
*/
|
|
112
|
+
export declare function appendDocumentLark(account: ResolvedLarkAccount, docToken: string, markdown: string): Promise<LarkAppendDocResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Update a single text block in a document.
|
|
115
|
+
*
|
|
116
|
+
* @param account - The Lark account to use
|
|
117
|
+
* @param docToken - Document token
|
|
118
|
+
* @param blockId - Block ID to update
|
|
119
|
+
* @param content - New text content
|
|
120
|
+
* @returns Update result
|
|
121
|
+
*/
|
|
122
|
+
export declare function updateBlockLark(account: ResolvedLarkAccount, docToken: string, blockId: string, content: string): Promise<{
|
|
123
|
+
success: boolean;
|
|
124
|
+
block_id: string;
|
|
125
|
+
}>;
|
|
126
|
+
/**
|
|
127
|
+
* Delete a block from a document.
|
|
128
|
+
*
|
|
129
|
+
* @param account - The Lark account to use
|
|
130
|
+
* @param docToken - Document token
|
|
131
|
+
* @param blockId - Block ID to delete
|
|
132
|
+
* @returns Delete result
|
|
133
|
+
*/
|
|
134
|
+
export declare function deleteBlockLark(account: ResolvedLarkAccount, docToken: string, blockId: string): Promise<{
|
|
135
|
+
success: boolean;
|
|
136
|
+
deleted_block_id: string;
|
|
137
|
+
}>;
|
|
138
|
+
/**
|
|
139
|
+
* List all blocks in a document.
|
|
140
|
+
*
|
|
141
|
+
* @param account - The Lark account to use
|
|
142
|
+
* @param docToken - Document token
|
|
143
|
+
* @returns List of all blocks with full data
|
|
144
|
+
*/
|
|
145
|
+
export declare function listBlocksLark(account: ResolvedLarkAccount, docToken: string): Promise<LarkListBlocksResult>;
|
|
146
|
+
/**
|
|
147
|
+
* Get a single block's details.
|
|
148
|
+
*
|
|
149
|
+
* @param account - The Lark account to use
|
|
150
|
+
* @param docToken - Document token
|
|
151
|
+
* @param blockId - Block ID
|
|
152
|
+
* @returns Block details
|
|
153
|
+
*/
|
|
154
|
+
export declare function getBlockLark(account: ResolvedLarkAccount, docToken: string, blockId: string): Promise<LarkGetBlockResult>;
|
|
155
|
+
/**
|
|
156
|
+
* List files in a folder.
|
|
157
|
+
*
|
|
158
|
+
* @param account - The Lark account to use
|
|
159
|
+
* @param folderToken - Folder token
|
|
160
|
+
* @returns List of files in folder
|
|
161
|
+
*/
|
|
162
|
+
export declare function listFolderLark(account: ResolvedLarkAccount, folderToken: string): Promise<LarkListFolderResult>;
|
|
163
|
+
/**
|
|
164
|
+
* List application scopes (permissions).
|
|
165
|
+
*
|
|
166
|
+
* @param account - The Lark account to use
|
|
167
|
+
* @returns List of granted and pending scopes
|
|
168
|
+
*/
|
|
169
|
+
export declare function listAppScopesLark(account: ResolvedLarkAccount): Promise<LarkAppScopeResult>;
|
package/dist/src/docx.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lark/Feishu Document Tools
|
|
3
|
+
*
|
|
4
|
+
* Functions for creating, reading, updating, and managing Lark documents.
|
|
5
|
+
* Supports markdown-to-document conversion with image handling.
|
|
6
|
+
*/
|
|
7
|
+
import { createLarkClient } from "./client.js";
|
|
8
|
+
import { getLarkRuntime } from "./runtime.js";
|
|
9
|
+
import { Readable } from "stream";
|
|
10
|
+
// ============ Constants ============
|
|
11
|
+
const BLOCK_TYPE_NAMES = {
|
|
12
|
+
1: "Page",
|
|
13
|
+
2: "Text",
|
|
14
|
+
3: "Heading1",
|
|
15
|
+
4: "Heading2",
|
|
16
|
+
5: "Heading3",
|
|
17
|
+
12: "Bullet",
|
|
18
|
+
13: "Ordered",
|
|
19
|
+
14: "Code",
|
|
20
|
+
15: "Quote",
|
|
21
|
+
17: "Todo",
|
|
22
|
+
18: "Bitable",
|
|
23
|
+
21: "Diagram",
|
|
24
|
+
22: "Divider",
|
|
25
|
+
23: "File",
|
|
26
|
+
27: "Image",
|
|
27
|
+
30: "Sheet",
|
|
28
|
+
31: "Table",
|
|
29
|
+
32: "TableCell",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Wrap API errors to avoid exposing sensitive details.
|
|
33
|
+
* Sanitizes error messages while preserving error codes.
|
|
34
|
+
*/
|
|
35
|
+
function wrapLarkError(message, apiError) {
|
|
36
|
+
if (apiError?.code !== undefined && apiError.code !== 0) {
|
|
37
|
+
// Include error code but sanitize the message
|
|
38
|
+
return new Error(`${message} (API error code: ${apiError.code})`);
|
|
39
|
+
}
|
|
40
|
+
return new Error(message);
|
|
41
|
+
}
|
|
42
|
+
// Block types that cannot be created via documentBlockChildren.create API
|
|
43
|
+
const UNSUPPORTED_CREATE_TYPES = new Set([
|
|
44
|
+
31, // Table - must use different API or workaround
|
|
45
|
+
32, // TableCell - child of Table
|
|
46
|
+
]);
|
|
47
|
+
// Block types that are NOT included in rawContent (plain text) output
|
|
48
|
+
const STRUCTURED_BLOCK_TYPES = new Set([14, 18, 21, 23, 27, 30, 31, 32]);
|
|
49
|
+
// ============ Helper Functions ============
|
|
50
|
+
function extractBlockPreview(block) {
|
|
51
|
+
const elements = block.text?.elements ??
|
|
52
|
+
block.heading1?.elements ??
|
|
53
|
+
block.heading2?.elements ??
|
|
54
|
+
block.heading3?.elements ??
|
|
55
|
+
[];
|
|
56
|
+
return elements
|
|
57
|
+
.filter((e) => e.text_run)
|
|
58
|
+
.map((e) => e.text_run.content)
|
|
59
|
+
.join("")
|
|
60
|
+
.slice(0, 50);
|
|
61
|
+
}
|
|
62
|
+
/** Extract image URLs from markdown content */
|
|
63
|
+
function extractImageUrls(markdown) {
|
|
64
|
+
const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
65
|
+
const urls = [];
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = regex.exec(markdown)) !== null) {
|
|
68
|
+
const url = match[1].trim();
|
|
69
|
+
// Only collect http(s) URLs, not file paths
|
|
70
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
71
|
+
urls.push(url);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return urls;
|
|
75
|
+
}
|
|
76
|
+
/** Clean blocks for insertion (remove unsupported types and read-only fields) */
|
|
77
|
+
function cleanBlocksForInsert(blocks) {
|
|
78
|
+
const skipped = [];
|
|
79
|
+
const cleaned = blocks
|
|
80
|
+
.filter((block) => {
|
|
81
|
+
if (UNSUPPORTED_CREATE_TYPES.has(block.block_type)) {
|
|
82
|
+
const typeName = BLOCK_TYPE_NAMES[block.block_type] || `type_${block.block_type}`;
|
|
83
|
+
skipped.push(typeName);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
})
|
|
88
|
+
.map((block) => {
|
|
89
|
+
// Remove any read-only fields that might slip through
|
|
90
|
+
if (block.block_type === 31 && block.table?.merge_info) {
|
|
91
|
+
const { merge_info, ...tableRest } = block.table;
|
|
92
|
+
return { ...block, table: tableRest };
|
|
93
|
+
}
|
|
94
|
+
return block;
|
|
95
|
+
});
|
|
96
|
+
return { cleaned, skipped };
|
|
97
|
+
}
|
|
98
|
+
/** Convert markdown to Lark blocks using the Convert API */
|
|
99
|
+
async function convertMarkdown(client, markdown) {
|
|
100
|
+
const res = await client.docx.document.convert({
|
|
101
|
+
data: { content_type: "markdown", content: markdown },
|
|
102
|
+
});
|
|
103
|
+
if (res.code !== 0)
|
|
104
|
+
throw wrapLarkError("Failed to convert markdown to document blocks", res);
|
|
105
|
+
return {
|
|
106
|
+
blocks: res.data?.blocks ?? [],
|
|
107
|
+
firstLevelBlockIds: res.data?.first_level_block_ids ?? [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Insert blocks as children of a parent block */
|
|
111
|
+
async function insertBlocks(client, docToken, blocks, parentBlockId) {
|
|
112
|
+
const { cleaned, skipped } = cleanBlocksForInsert(blocks);
|
|
113
|
+
const blockId = parentBlockId ?? docToken;
|
|
114
|
+
if (cleaned.length === 0) {
|
|
115
|
+
return { children: [], skipped };
|
|
116
|
+
}
|
|
117
|
+
const res = await client.docx.documentBlockChildren.create({
|
|
118
|
+
path: { document_id: docToken, block_id: blockId },
|
|
119
|
+
data: { children: cleaned },
|
|
120
|
+
});
|
|
121
|
+
if (res.code !== 0)
|
|
122
|
+
throw wrapLarkError("Failed to insert blocks into document", res);
|
|
123
|
+
return { children: res.data?.children ?? [], skipped };
|
|
124
|
+
}
|
|
125
|
+
/** Delete all child blocks from a parent */
|
|
126
|
+
async function clearDocumentContent(client, docToken) {
|
|
127
|
+
const existing = await client.docx.documentBlock.list({
|
|
128
|
+
path: { document_id: docToken },
|
|
129
|
+
});
|
|
130
|
+
if (existing.code !== 0)
|
|
131
|
+
throw wrapLarkError("Failed to list document blocks", existing);
|
|
132
|
+
const childIds = existing.data?.items
|
|
133
|
+
?.filter((b) => b.parent_id === docToken && b.block_type !== 1)
|
|
134
|
+
.map((b) => b.block_id) ?? [];
|
|
135
|
+
if (childIds.length > 0) {
|
|
136
|
+
const res = await client.docx.documentBlockChildren.batchDelete({
|
|
137
|
+
path: { document_id: docToken, block_id: docToken },
|
|
138
|
+
data: { start_index: 0, end_index: childIds.length },
|
|
139
|
+
});
|
|
140
|
+
if (res.code !== 0)
|
|
141
|
+
throw wrapLarkError("Failed to clear document content", res);
|
|
142
|
+
}
|
|
143
|
+
return childIds.length;
|
|
144
|
+
}
|
|
145
|
+
/** Upload image to Lark drive for docx */
|
|
146
|
+
async function uploadImageToDocx(client, blockId, imageBuffer, fileName) {
|
|
147
|
+
const res = await client.drive.media.uploadAll({
|
|
148
|
+
data: {
|
|
149
|
+
file_name: fileName,
|
|
150
|
+
parent_type: "docx_image",
|
|
151
|
+
parent_node: blockId,
|
|
152
|
+
size: imageBuffer.length,
|
|
153
|
+
file: Readable.from(imageBuffer),
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const fileToken = res?.file_token;
|
|
157
|
+
if (!fileToken) {
|
|
158
|
+
throw new Error("Image upload failed: no file_token returned");
|
|
159
|
+
}
|
|
160
|
+
return fileToken;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Validate URL and fetch with timeout.
|
|
164
|
+
* Only allows http/https protocols to prevent SSRF attacks.
|
|
165
|
+
*/
|
|
166
|
+
async function validatedFetch(url, timeoutMs = 30000) {
|
|
167
|
+
let parsedUrl;
|
|
168
|
+
try {
|
|
169
|
+
parsedUrl = new URL(url);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
throw new Error(`Invalid URL: ${url}`);
|
|
173
|
+
}
|
|
174
|
+
// Only allow http/https protocols
|
|
175
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
176
|
+
throw new Error(`URL protocol not allowed: ${parsedUrl.protocol}. ` +
|
|
177
|
+
`Only http and https are supported.`);
|
|
178
|
+
}
|
|
179
|
+
// Create AbortController for timeout
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(url, {
|
|
184
|
+
signal: controller.signal,
|
|
185
|
+
redirect: "follow",
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
189
|
+
}
|
|
190
|
+
return Buffer.from(await response.arrayBuffer());
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
194
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
195
|
+
}
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
clearTimeout(timeoutId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Download image from URL with validation */
|
|
203
|
+
async function downloadImage(url) {
|
|
204
|
+
return validatedFetch(url, 30000);
|
|
205
|
+
}
|
|
206
|
+
/** Process images in markdown: download from URL, upload to Lark, update blocks */
|
|
207
|
+
async function processImages(client, docToken, markdown, insertedBlocks) {
|
|
208
|
+
const imageUrls = extractImageUrls(markdown);
|
|
209
|
+
if (imageUrls.length === 0)
|
|
210
|
+
return { processed: 0, failed: 0, errors: [] };
|
|
211
|
+
// Find Image blocks (block_type 27)
|
|
212
|
+
const imageBlocks = insertedBlocks.filter((b) => b.block_type === 27);
|
|
213
|
+
let processed = 0;
|
|
214
|
+
const errors = [];
|
|
215
|
+
for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
|
|
216
|
+
const url = imageUrls[i];
|
|
217
|
+
const blockId = imageBlocks[i].block_id;
|
|
218
|
+
try {
|
|
219
|
+
// Download image from URL
|
|
220
|
+
const buffer = await downloadImage(url);
|
|
221
|
+
// Generate filename from URL
|
|
222
|
+
const urlPath = new URL(url).pathname;
|
|
223
|
+
const fileName = urlPath.split("/").pop() || `image_${i}.png`;
|
|
224
|
+
// Upload to Lark
|
|
225
|
+
const fileToken = await uploadImageToDocx(client, blockId, buffer, fileName);
|
|
226
|
+
// Update the image block
|
|
227
|
+
await client.docx.documentBlock.patch({
|
|
228
|
+
path: { document_id: docToken, block_id: blockId },
|
|
229
|
+
data: {
|
|
230
|
+
replace_image: { token: fileToken },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
processed++;
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
// Track error but continue processing other images
|
|
237
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
errors.push({ url, error: errorMsg });
|
|
239
|
+
getLarkRuntime().logger.warn(`[lark] Failed to process image ${url}: ${errorMsg}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { processed, failed: errors.length, errors };
|
|
243
|
+
}
|
|
244
|
+
// ============ Public API Functions ============
|
|
245
|
+
/**
|
|
246
|
+
* Read a Lark document's content and metadata.
|
|
247
|
+
*
|
|
248
|
+
* @param account - The Lark account to use
|
|
249
|
+
* @param docToken - The document token (from URL /docx/XXX)
|
|
250
|
+
* @returns Document info with title, content, block counts
|
|
251
|
+
*/
|
|
252
|
+
export async function readDocumentLark(account, docToken) {
|
|
253
|
+
const client = createLarkClient(account);
|
|
254
|
+
const [contentRes, infoRes, blocksRes] = await Promise.all([
|
|
255
|
+
client.docx.document.rawContent({ path: { document_id: docToken } }),
|
|
256
|
+
client.docx.document.get({ path: { document_id: docToken } }),
|
|
257
|
+
client.docx.documentBlock.list({ path: { document_id: docToken } }),
|
|
258
|
+
]);
|
|
259
|
+
if (contentRes.code !== 0)
|
|
260
|
+
throw wrapLarkError("Failed to read document content", contentRes);
|
|
261
|
+
const blocks = blocksRes.data?.items ?? [];
|
|
262
|
+
const blockCounts = {};
|
|
263
|
+
const structuredTypes = [];
|
|
264
|
+
for (const b of blocks) {
|
|
265
|
+
const type = b.block_type ?? 0;
|
|
266
|
+
const name = BLOCK_TYPE_NAMES[type] || `type_${type}`;
|
|
267
|
+
blockCounts[name] = (blockCounts[name] || 0) + 1;
|
|
268
|
+
// Track structured types that need list_blocks to read
|
|
269
|
+
if (STRUCTURED_BLOCK_TYPES.has(type) && !structuredTypes.includes(name)) {
|
|
270
|
+
structuredTypes.push(name);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Build hint if there are structured blocks
|
|
274
|
+
let hint;
|
|
275
|
+
if (structuredTypes.length > 0) {
|
|
276
|
+
hint = `This document contains ${structuredTypes.join(", ")} which are NOT included in the plain text above. Use listBlocksLark to get full content.`;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
title: infoRes.data?.document?.title,
|
|
280
|
+
content: contentRes.data?.content,
|
|
281
|
+
revision_id: infoRes.data?.document?.revision_id,
|
|
282
|
+
block_count: blocks.length,
|
|
283
|
+
block_types: blockCounts,
|
|
284
|
+
...(hint && { hint }),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Create a new Lark document.
|
|
289
|
+
*
|
|
290
|
+
* @param account - The Lark account to use
|
|
291
|
+
* @param title - Document title
|
|
292
|
+
* @param folderToken - Optional parent folder token
|
|
293
|
+
* @returns Created document info with ID and URL
|
|
294
|
+
*/
|
|
295
|
+
export async function createDocumentLark(account, title, folderToken) {
|
|
296
|
+
const client = createLarkClient(account);
|
|
297
|
+
const res = await client.docx.document.create({
|
|
298
|
+
data: { title, folder_token: folderToken },
|
|
299
|
+
});
|
|
300
|
+
if (res.code !== 0)
|
|
301
|
+
throw wrapLarkError("Failed to create document", res);
|
|
302
|
+
const doc = res.data?.document;
|
|
303
|
+
// Build URL based on domain
|
|
304
|
+
const baseUrl = account.domain === "lark" ? "https://larksuite.com" : "https://feishu.cn";
|
|
305
|
+
return {
|
|
306
|
+
document_id: doc?.document_id,
|
|
307
|
+
title: doc?.title,
|
|
308
|
+
url: `${baseUrl}/docx/${doc?.document_id}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Write markdown content to a Lark document (replaces all content).
|
|
313
|
+
*
|
|
314
|
+
* @param account - The Lark account to use
|
|
315
|
+
* @param docToken - Document token
|
|
316
|
+
* @param markdown - Markdown content to write
|
|
317
|
+
* @returns Write result with block counts and warnings
|
|
318
|
+
*/
|
|
319
|
+
export async function writeDocumentLark(account, docToken, markdown) {
|
|
320
|
+
const client = createLarkClient(account);
|
|
321
|
+
// 1. Clear existing content
|
|
322
|
+
const deleted = await clearDocumentContent(client, docToken);
|
|
323
|
+
// 2. Convert markdown to blocks
|
|
324
|
+
const { blocks } = await convertMarkdown(client, markdown);
|
|
325
|
+
if (blocks.length === 0) {
|
|
326
|
+
return { success: true, blocks_deleted: deleted, blocks_added: 0, images_processed: 0, images_failed: 0, image_errors: [] };
|
|
327
|
+
}
|
|
328
|
+
// 3. Insert new blocks (unsupported types like Table are filtered)
|
|
329
|
+
const { children: inserted, skipped } = await insertBlocks(client, docToken, blocks);
|
|
330
|
+
// 4. Process images
|
|
331
|
+
const { processed: imagesProcessed, failed: imagesFailed, errors: imageErrors } = await processImages(client, docToken, markdown, inserted);
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
blocks_deleted: deleted,
|
|
335
|
+
blocks_added: inserted.length,
|
|
336
|
+
images_processed: imagesProcessed,
|
|
337
|
+
images_failed: imagesFailed,
|
|
338
|
+
image_errors: imageErrors,
|
|
339
|
+
...(skipped.length > 0 && {
|
|
340
|
+
warning: `Skipped unsupported block types: ${skipped.join(", ")}. Tables are not supported via this API.`,
|
|
341
|
+
}),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Append markdown content to a Lark document.
|
|
346
|
+
*
|
|
347
|
+
* @param account - The Lark account to use
|
|
348
|
+
* @param docToken - Document token
|
|
349
|
+
* @param markdown - Markdown content to append
|
|
350
|
+
* @returns Append result with block counts
|
|
351
|
+
*/
|
|
352
|
+
export async function appendDocumentLark(account, docToken, markdown) {
|
|
353
|
+
const client = createLarkClient(account);
|
|
354
|
+
// 1. Convert markdown to blocks
|
|
355
|
+
const { blocks } = await convertMarkdown(client, markdown);
|
|
356
|
+
if (blocks.length === 0) {
|
|
357
|
+
throw new Error("Content is empty");
|
|
358
|
+
}
|
|
359
|
+
// 2. Insert blocks (unsupported types like Table are filtered)
|
|
360
|
+
const { children: inserted, skipped } = await insertBlocks(client, docToken, blocks);
|
|
361
|
+
// 3. Process images
|
|
362
|
+
const { processed: imagesProcessed, failed: imagesFailed, errors: imageErrors } = await processImages(client, docToken, markdown, inserted);
|
|
363
|
+
return {
|
|
364
|
+
success: true,
|
|
365
|
+
blocks_added: inserted.length,
|
|
366
|
+
images_processed: imagesProcessed,
|
|
367
|
+
images_failed: imagesFailed,
|
|
368
|
+
image_errors: imageErrors,
|
|
369
|
+
block_ids: inserted.map((b) => b.block_id),
|
|
370
|
+
...(skipped.length > 0 && {
|
|
371
|
+
warning: `Skipped unsupported block types: ${skipped.join(", ")}. Tables are not supported via this API.`,
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Update a single text block in a document.
|
|
377
|
+
*
|
|
378
|
+
* @param account - The Lark account to use
|
|
379
|
+
* @param docToken - Document token
|
|
380
|
+
* @param blockId - Block ID to update
|
|
381
|
+
* @param content - New text content
|
|
382
|
+
* @returns Update result
|
|
383
|
+
*/
|
|
384
|
+
export async function updateBlockLark(account, docToken, blockId, content) {
|
|
385
|
+
const client = createLarkClient(account);
|
|
386
|
+
const blockInfo = await client.docx.documentBlock.get({
|
|
387
|
+
path: { document_id: docToken, block_id: blockId },
|
|
388
|
+
});
|
|
389
|
+
if (blockInfo.code !== 0)
|
|
390
|
+
throw wrapLarkError("Failed to get block info", blockInfo);
|
|
391
|
+
const res = await client.docx.documentBlock.patch({
|
|
392
|
+
path: { document_id: docToken, block_id: blockId },
|
|
393
|
+
data: {
|
|
394
|
+
update_text_elements: {
|
|
395
|
+
elements: [{ text_run: { content } }],
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
if (res.code !== 0)
|
|
400
|
+
throw wrapLarkError("Failed to update block", res);
|
|
401
|
+
return { success: true, block_id: blockId };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Delete a block from a document.
|
|
405
|
+
*
|
|
406
|
+
* @param account - The Lark account to use
|
|
407
|
+
* @param docToken - Document token
|
|
408
|
+
* @param blockId - Block ID to delete
|
|
409
|
+
* @returns Delete result
|
|
410
|
+
*/
|
|
411
|
+
export async function deleteBlockLark(account, docToken, blockId) {
|
|
412
|
+
const client = createLarkClient(account);
|
|
413
|
+
const blockInfo = await client.docx.documentBlock.get({
|
|
414
|
+
path: { document_id: docToken, block_id: blockId },
|
|
415
|
+
});
|
|
416
|
+
if (blockInfo.code !== 0)
|
|
417
|
+
throw wrapLarkError("Failed to get block info", blockInfo);
|
|
418
|
+
const parentId = blockInfo.data?.block?.parent_id ?? docToken;
|
|
419
|
+
const children = await client.docx.documentBlockChildren.get({
|
|
420
|
+
path: { document_id: docToken, block_id: parentId },
|
|
421
|
+
});
|
|
422
|
+
if (children.code !== 0)
|
|
423
|
+
throw wrapLarkError("Failed to get block children", children);
|
|
424
|
+
const items = children.data?.items ?? [];
|
|
425
|
+
const index = items.findIndex((item) => item.block_id === blockId);
|
|
426
|
+
if (index === -1)
|
|
427
|
+
throw new Error("Block not found");
|
|
428
|
+
const res = await client.docx.documentBlockChildren.batchDelete({
|
|
429
|
+
path: { document_id: docToken, block_id: parentId },
|
|
430
|
+
data: { start_index: index, end_index: index + 1 },
|
|
431
|
+
});
|
|
432
|
+
if (res.code !== 0)
|
|
433
|
+
throw wrapLarkError("Failed to delete block", res);
|
|
434
|
+
return { success: true, deleted_block_id: blockId };
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* List all blocks in a document.
|
|
438
|
+
*
|
|
439
|
+
* @param account - The Lark account to use
|
|
440
|
+
* @param docToken - Document token
|
|
441
|
+
* @returns List of all blocks with full data
|
|
442
|
+
*/
|
|
443
|
+
export async function listBlocksLark(account, docToken) {
|
|
444
|
+
const client = createLarkClient(account);
|
|
445
|
+
const res = await client.docx.documentBlock.list({
|
|
446
|
+
path: { document_id: docToken },
|
|
447
|
+
});
|
|
448
|
+
if (res.code !== 0)
|
|
449
|
+
throw wrapLarkError("Failed to list blocks", res);
|
|
450
|
+
// Return full block data for agent to parse
|
|
451
|
+
return {
|
|
452
|
+
blocks: res.data?.items ?? [],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get a single block's details.
|
|
457
|
+
*
|
|
458
|
+
* @param account - The Lark account to use
|
|
459
|
+
* @param docToken - Document token
|
|
460
|
+
* @param blockId - Block ID
|
|
461
|
+
* @returns Block details
|
|
462
|
+
*/
|
|
463
|
+
export async function getBlockLark(account, docToken, blockId) {
|
|
464
|
+
const client = createLarkClient(account);
|
|
465
|
+
const res = await client.docx.documentBlock.get({
|
|
466
|
+
path: { document_id: docToken, block_id: blockId },
|
|
467
|
+
});
|
|
468
|
+
if (res.code !== 0)
|
|
469
|
+
throw wrapLarkError("Failed to get block", res);
|
|
470
|
+
return {
|
|
471
|
+
block: res.data?.block,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* List files in a folder.
|
|
476
|
+
*
|
|
477
|
+
* @param account - The Lark account to use
|
|
478
|
+
* @param folderToken - Folder token
|
|
479
|
+
* @returns List of files in folder
|
|
480
|
+
*/
|
|
481
|
+
export async function listFolderLark(account, folderToken) {
|
|
482
|
+
const client = createLarkClient(account);
|
|
483
|
+
const res = await client.drive.file.list({
|
|
484
|
+
params: { folder_token: folderToken },
|
|
485
|
+
});
|
|
486
|
+
if (res.code !== 0)
|
|
487
|
+
throw wrapLarkError("Failed to list folder", res);
|
|
488
|
+
return {
|
|
489
|
+
files: res.data?.files?.map((f) => ({
|
|
490
|
+
token: f.token,
|
|
491
|
+
name: f.name,
|
|
492
|
+
type: f.type,
|
|
493
|
+
url: f.url,
|
|
494
|
+
})) ?? [],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* List application scopes (permissions).
|
|
499
|
+
*
|
|
500
|
+
* @param account - The Lark account to use
|
|
501
|
+
* @returns List of granted and pending scopes
|
|
502
|
+
*/
|
|
503
|
+
export async function listAppScopesLark(account) {
|
|
504
|
+
const client = createLarkClient(account);
|
|
505
|
+
const res = await client.application.scope.list({});
|
|
506
|
+
if (res.code !== 0)
|
|
507
|
+
throw wrapLarkError("Failed to list app scopes", res);
|
|
508
|
+
const scopes = res.data?.scopes ?? [];
|
|
509
|
+
const granted = scopes.filter((s) => s.grant_status === 1);
|
|
510
|
+
const pending = scopes.filter((s) => s.grant_status !== 1);
|
|
511
|
+
return {
|
|
512
|
+
granted: granted.map((s) => ({ name: s.scope_name, type: s.scope_type })),
|
|
513
|
+
pending: pending.map((s) => ({ name: s.scope_name, type: s.scope_type })),
|
|
514
|
+
summary: `${granted.length} granted, ${pending.length} pending`,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
//# sourceMappingURL=docx.js.map
|