@max1874/feishu 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/docx.ts +237 -87
package/package.json
CHANGED
package/src/docx.ts
CHANGED
|
@@ -129,12 +129,63 @@ interface TableData {
|
|
|
129
129
|
cells: string[][]; // 2D array of cell text content
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
/** A content item that can be either a regular block or a table */
|
|
133
|
+
type ContentItem =
|
|
134
|
+
| { type: "block"; block: any }
|
|
135
|
+
| { type: "table"; table: TableData };
|
|
136
|
+
|
|
137
|
+
/** Extract table data from a table block */
|
|
138
|
+
function extractTableData(tableBlock: any, blockMap: Map<string, any>): { tableData: TableData; relatedIds: Set<string> } {
|
|
139
|
+
const relatedIds = new Set<string>();
|
|
140
|
+
relatedIds.add(tableBlock.block_id);
|
|
141
|
+
|
|
142
|
+
const tableInfo = tableBlock.table;
|
|
143
|
+
const rowSize = tableInfo?.property?.row_size ?? 0;
|
|
144
|
+
const colSize = tableInfo?.property?.column_size ?? 0;
|
|
145
|
+
|
|
146
|
+
const cells: string[][] = Array.from({ length: rowSize }, () =>
|
|
147
|
+
Array.from({ length: colSize }, () => ""),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const cellIds: string[] = tableInfo?.cells ?? [];
|
|
151
|
+
let cellIndex = 0;
|
|
152
|
+
|
|
153
|
+
for (let row = 0; row < rowSize; row++) {
|
|
154
|
+
for (let col = 0; col < colSize; col++) {
|
|
155
|
+
if (cellIndex >= cellIds.length) break;
|
|
156
|
+
|
|
157
|
+
const cellId = cellIds[cellIndex];
|
|
158
|
+
relatedIds.add(cellId);
|
|
159
|
+
|
|
160
|
+
const cellBlock = blockMap.get(cellId);
|
|
161
|
+
if (cellBlock?.block_type === TABLE_CELL_BLOCK_TYPE) {
|
|
162
|
+
const childIds: string[] = cellBlock.children ?? [];
|
|
163
|
+
const textParts: string[] = [];
|
|
164
|
+
|
|
165
|
+
for (const childId of childIds) {
|
|
166
|
+
relatedIds.add(childId);
|
|
167
|
+
const textBlock = blockMap.get(childId);
|
|
168
|
+
if (textBlock?.block_type === 2 && textBlock.text?.elements) {
|
|
169
|
+
const text = textBlock.text.elements
|
|
170
|
+
.filter((e: any) => e.text_run)
|
|
171
|
+
.map((e: any) => e.text_run.content)
|
|
172
|
+
.join("");
|
|
173
|
+
textParts.push(text);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
cells[row][col] = textParts.join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
cellIndex++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
136
183
|
|
|
137
|
-
|
|
184
|
+
return { tableData: { rowSize, colSize, cells }, relatedIds };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Convert blocks to content items, preserving order with tables in correct positions */
|
|
188
|
+
function convertBlocksToContentItems(blocks: any[]): ContentItem[] {
|
|
138
189
|
const blockMap = new Map<string, any>();
|
|
139
190
|
for (const block of blocks) {
|
|
140
191
|
if (block.block_id) {
|
|
@@ -142,69 +193,52 @@ function extractTableFromBlocks(blocks: any[]): { tables: TableData[]; otherBloc
|
|
|
142
193
|
}
|
|
143
194
|
}
|
|
144
195
|
|
|
145
|
-
// Track which blocks are part of tables
|
|
196
|
+
// Track which blocks are part of tables
|
|
146
197
|
const tableRelatedIds = new Set<string>();
|
|
198
|
+
const tableDataMap = new Map<string, TableData>();
|
|
147
199
|
|
|
200
|
+
// First pass: extract all table data and mark related IDs
|
|
148
201
|
for (const block of blocks) {
|
|
149
202
|
if (block.block_type === TABLE_BLOCK_TYPE) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const rowSize = tableInfo.property?.row_size ?? 0;
|
|
155
|
-
const colSize = tableInfo.property?.column_size ?? 0;
|
|
156
|
-
|
|
157
|
-
// Initialize cells array
|
|
158
|
-
const cells: string[][] = Array.from({ length: rowSize }, () =>
|
|
159
|
-
Array.from({ length: colSize }, () => ""),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// table.cells is a flat array of cell block IDs in row-major order
|
|
163
|
-
const cellIds: string[] = tableInfo.cells ?? [];
|
|
164
|
-
|
|
165
|
-
let cellIndex = 0;
|
|
166
|
-
for (let row = 0; row < rowSize; row++) {
|
|
167
|
-
for (let col = 0; col < colSize; col++) {
|
|
168
|
-
if (cellIndex >= cellIds.length) break;
|
|
169
|
-
|
|
170
|
-
const cellId = cellIds[cellIndex];
|
|
171
|
-
tableRelatedIds.add(cellId);
|
|
172
|
-
|
|
173
|
-
// Find the TableCell block
|
|
174
|
-
const cellBlock = blockMap.get(cellId);
|
|
175
|
-
if (cellBlock?.block_type === TABLE_CELL_BLOCK_TYPE) {
|
|
176
|
-
// Get text content from cell's children (Text blocks)
|
|
177
|
-
const childIds: string[] = cellBlock.children ?? [];
|
|
178
|
-
const textParts: string[] = [];
|
|
179
|
-
|
|
180
|
-
for (const childId of childIds) {
|
|
181
|
-
tableRelatedIds.add(childId);
|
|
182
|
-
const textBlock = blockMap.get(childId);
|
|
183
|
-
if (textBlock?.block_type === 2 && textBlock.text?.elements) {
|
|
184
|
-
const text = textBlock.text.elements
|
|
185
|
-
.filter((e: any) => e.text_run)
|
|
186
|
-
.map((e: any) => e.text_run.content)
|
|
187
|
-
.join("");
|
|
188
|
-
textParts.push(text);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
cells[row][col] = textParts.join("\n");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
cellIndex++;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
tables.push({ rowSize, colSize, cells });
|
|
203
|
+
const { tableData, relatedIds } = extractTableData(block, blockMap);
|
|
204
|
+
tableDataMap.set(block.block_id, tableData);
|
|
205
|
+
for (const id of relatedIds) {
|
|
206
|
+
tableRelatedIds.add(id);
|
|
200
207
|
}
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
//
|
|
211
|
+
// Second pass: build content items in order
|
|
212
|
+
const items: ContentItem[] = [];
|
|
205
213
|
for (const block of blocks) {
|
|
206
|
-
if (
|
|
207
|
-
|
|
214
|
+
if (tableRelatedIds.has(block.block_id)) {
|
|
215
|
+
// If this is a table block itself, add the table item
|
|
216
|
+
if (block.block_type === TABLE_BLOCK_TYPE) {
|
|
217
|
+
const tableData = tableDataMap.get(block.block_id);
|
|
218
|
+
if (tableData) {
|
|
219
|
+
items.push({ type: "table", table: tableData });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Skip table-related blocks (cells, cell contents)
|
|
223
|
+
} else {
|
|
224
|
+
items.push({ type: "block", block });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return items;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Extract table data from converted blocks (flat list with ID references) - legacy function */
|
|
232
|
+
function extractTableFromBlocks(blocks: any[]): { tables: TableData[]; otherBlocks: any[] } {
|
|
233
|
+
const items = convertBlocksToContentItems(blocks);
|
|
234
|
+
const tables: TableData[] = [];
|
|
235
|
+
const otherBlocks: any[] = [];
|
|
236
|
+
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
if (item.type === "table") {
|
|
239
|
+
tables.push(item.table);
|
|
240
|
+
} else {
|
|
241
|
+
otherBlocks.push(item.block);
|
|
208
242
|
}
|
|
209
243
|
}
|
|
210
244
|
|
|
@@ -289,6 +323,40 @@ async function insertBlocks(
|
|
|
289
323
|
return { children: allChildren, skipped };
|
|
290
324
|
}
|
|
291
325
|
|
|
326
|
+
/** Insert blocks at a specific index (with batching for >50 blocks) */
|
|
327
|
+
async function insertBlocksAtIndex(
|
|
328
|
+
client: Lark.Client,
|
|
329
|
+
docToken: string,
|
|
330
|
+
blocks: any[],
|
|
331
|
+
startIndex: number,
|
|
332
|
+
parentBlockId?: string,
|
|
333
|
+
): Promise<{ children: any[]; skipped: string[] }> {
|
|
334
|
+
const { cleaned, skipped } = cleanBlocksForInsert(blocks);
|
|
335
|
+
const blockId = parentBlockId ?? docToken;
|
|
336
|
+
|
|
337
|
+
if (cleaned.length === 0) {
|
|
338
|
+
return { children: [], skipped };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const BATCH_SIZE = 50;
|
|
342
|
+
const allChildren: any[] = [];
|
|
343
|
+
let insertIndex = startIndex;
|
|
344
|
+
|
|
345
|
+
for (let i = 0; i < cleaned.length; i += BATCH_SIZE) {
|
|
346
|
+
const batch = cleaned.slice(i, i + BATCH_SIZE);
|
|
347
|
+
const res = await client.docx.documentBlockChildren.create({
|
|
348
|
+
path: { document_id: docToken, block_id: blockId },
|
|
349
|
+
data: { children: batch, index: insertIndex },
|
|
350
|
+
});
|
|
351
|
+
if (res.code !== 0) throw new Error(res.msg);
|
|
352
|
+
const inserted = res.data?.children ?? [];
|
|
353
|
+
allChildren.push(...inserted);
|
|
354
|
+
insertIndex += inserted.length;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { children: allChildren, skipped };
|
|
358
|
+
}
|
|
359
|
+
|
|
292
360
|
/** Delete all child blocks from a parent */
|
|
293
361
|
async function clearDocumentContent(client: Lark.Client, docToken: string) {
|
|
294
362
|
const existing = await client.docx.documentBlock.list({
|
|
@@ -352,6 +420,7 @@ async function createEmptyTable(
|
|
|
352
420
|
parentBlockId: string,
|
|
353
421
|
rowSize: number,
|
|
354
422
|
colSize: number,
|
|
423
|
+
index?: number,
|
|
355
424
|
): Promise<string | null> {
|
|
356
425
|
const tableBlock = {
|
|
357
426
|
block_type: TABLE_BLOCK_TYPE,
|
|
@@ -367,7 +436,7 @@ async function createEmptyTable(
|
|
|
367
436
|
|
|
368
437
|
const res = await client.docx.documentBlockChildren.create({
|
|
369
438
|
path: { document_id: docToken, block_id: parentBlockId },
|
|
370
|
-
data: { children: [tableBlock] },
|
|
439
|
+
data: { children: [tableBlock], ...(index !== undefined && { index }) },
|
|
371
440
|
});
|
|
372
441
|
|
|
373
442
|
if (res.code !== 0) {
|
|
@@ -479,6 +548,34 @@ async function createAndFillTable(
|
|
|
479
548
|
return true;
|
|
480
549
|
}
|
|
481
550
|
|
|
551
|
+
/** Create and fill a table at a specific index */
|
|
552
|
+
async function createAndFillTableAtIndex(
|
|
553
|
+
client: Lark.Client,
|
|
554
|
+
docToken: string,
|
|
555
|
+
parentBlockId: string,
|
|
556
|
+
tableData: TableData,
|
|
557
|
+
index: number,
|
|
558
|
+
): Promise<boolean> {
|
|
559
|
+
// 1. Create empty table at specific index
|
|
560
|
+
const tableBlockId = await createEmptyTable(
|
|
561
|
+
client,
|
|
562
|
+
docToken,
|
|
563
|
+
parentBlockId,
|
|
564
|
+
tableData.rowSize,
|
|
565
|
+
tableData.colSize,
|
|
566
|
+
index,
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
if (!tableBlockId) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 2. Fill table content
|
|
574
|
+
await fillTableContent(client, docToken, tableBlockId, tableData.cells);
|
|
575
|
+
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
482
579
|
/** Process images in markdown: download from URL, upload to Feishu, update blocks */
|
|
483
580
|
async function processImages(
|
|
484
581
|
client: Lark.Client,
|
|
@@ -719,30 +816,51 @@ async function writeDoc(client: Lark.Client, docToken: string, markdown: string)
|
|
|
719
816
|
return { success: true, blocks_deleted: deleted, blocks_added: 0, images_processed: 0, tables_created: 0 };
|
|
720
817
|
}
|
|
721
818
|
|
|
722
|
-
// 3.
|
|
723
|
-
const
|
|
819
|
+
// 3. Convert to content items (preserves table positions)
|
|
820
|
+
const contentItems = convertBlocksToContentItems(blocks);
|
|
724
821
|
|
|
725
|
-
// 4. Insert
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
822
|
+
// 4. Insert content items in order
|
|
823
|
+
let insertIndex = 0;
|
|
824
|
+
const allInserted: any[] = [];
|
|
825
|
+
const allSkipped: string[] = [];
|
|
729
826
|
let tablesCreated = 0;
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
827
|
+
let pendingBlocks: any[] = [];
|
|
828
|
+
|
|
829
|
+
const flushPendingBlocks = async () => {
|
|
830
|
+
if (pendingBlocks.length === 0) return;
|
|
831
|
+
const { children, skipped } = await insertBlocksAtIndex(client, docToken, pendingBlocks, insertIndex);
|
|
832
|
+
allInserted.push(...children);
|
|
833
|
+
allSkipped.push(...skipped);
|
|
834
|
+
insertIndex += children.length;
|
|
835
|
+
pendingBlocks = [];
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
for (const item of contentItems) {
|
|
839
|
+
if (item.type === "block") {
|
|
840
|
+
pendingBlocks.push(item.block);
|
|
841
|
+
} else if (item.type === "table") {
|
|
842
|
+
await flushPendingBlocks();
|
|
843
|
+
const success = await createAndFillTableAtIndex(client, docToken, docToken, item.table, insertIndex);
|
|
844
|
+
if (success) {
|
|
845
|
+
tablesCreated++;
|
|
846
|
+
insertIndex++;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
733
849
|
}
|
|
734
850
|
|
|
735
|
-
|
|
736
|
-
|
|
851
|
+
await flushPendingBlocks();
|
|
852
|
+
|
|
853
|
+
// 5. Process images
|
|
854
|
+
const imagesProcessed = await processImages(client, docToken, markdown, allInserted);
|
|
737
855
|
|
|
738
856
|
return {
|
|
739
857
|
success: true,
|
|
740
858
|
blocks_deleted: deleted,
|
|
741
|
-
blocks_added:
|
|
859
|
+
blocks_added: allInserted.length,
|
|
742
860
|
tables_created: tablesCreated,
|
|
743
861
|
images_processed: imagesProcessed,
|
|
744
|
-
...(
|
|
745
|
-
warning: `Skipped unsupported block types: ${
|
|
862
|
+
...(allSkipped.length > 0 && {
|
|
863
|
+
warning: `Skipped unsupported block types: ${allSkipped.join(", ")}.`,
|
|
746
864
|
}),
|
|
747
865
|
};
|
|
748
866
|
}
|
|
@@ -754,30 +872,62 @@ async function appendDoc(client: Lark.Client, docToken: string, markdown: string
|
|
|
754
872
|
throw new Error("Content is empty");
|
|
755
873
|
}
|
|
756
874
|
|
|
757
|
-
// 2.
|
|
758
|
-
const
|
|
875
|
+
// 2. Convert to content items (preserves table positions)
|
|
876
|
+
const contentItems = convertBlocksToContentItems(blocks);
|
|
759
877
|
|
|
760
|
-
// 3.
|
|
761
|
-
|
|
878
|
+
// 3. Get current insert index
|
|
879
|
+
let insertIndex = 0;
|
|
880
|
+
const existing = await client.docx.documentBlockChildren.get({
|
|
881
|
+
path: { document_id: docToken, block_id: docToken },
|
|
882
|
+
});
|
|
883
|
+
if (existing.code === 0) {
|
|
884
|
+
insertIndex = existing.data?.items?.length ?? 0;
|
|
885
|
+
}
|
|
762
886
|
|
|
763
|
-
// 4.
|
|
887
|
+
// 4. Insert content items in order, batching consecutive blocks
|
|
888
|
+
const allInserted: any[] = [];
|
|
889
|
+
const allSkipped: string[] = [];
|
|
764
890
|
let tablesCreated = 0;
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
891
|
+
let pendingBlocks: any[] = [];
|
|
892
|
+
|
|
893
|
+
const flushPendingBlocks = async () => {
|
|
894
|
+
if (pendingBlocks.length === 0) return;
|
|
895
|
+
const { children, skipped } = await insertBlocksAtIndex(client, docToken, pendingBlocks, insertIndex);
|
|
896
|
+
allInserted.push(...children);
|
|
897
|
+
allSkipped.push(...skipped);
|
|
898
|
+
insertIndex += children.length;
|
|
899
|
+
pendingBlocks = [];
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
for (const item of contentItems) {
|
|
903
|
+
if (item.type === "block") {
|
|
904
|
+
pendingBlocks.push(item.block);
|
|
905
|
+
} else if (item.type === "table") {
|
|
906
|
+
// Flush pending blocks before creating table
|
|
907
|
+
await flushPendingBlocks();
|
|
908
|
+
// Create table at current position
|
|
909
|
+
const success = await createAndFillTableAtIndex(client, docToken, docToken, item.table, insertIndex);
|
|
910
|
+
if (success) {
|
|
911
|
+
tablesCreated++;
|
|
912
|
+
insertIndex++;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
768
915
|
}
|
|
769
916
|
|
|
917
|
+
// Flush any remaining blocks
|
|
918
|
+
await flushPendingBlocks();
|
|
919
|
+
|
|
770
920
|
// 5. Process images
|
|
771
|
-
const imagesProcessed = await processImages(client, docToken, markdown,
|
|
921
|
+
const imagesProcessed = await processImages(client, docToken, markdown, allInserted);
|
|
772
922
|
|
|
773
923
|
return {
|
|
774
924
|
success: true,
|
|
775
|
-
blocks_added:
|
|
925
|
+
blocks_added: allInserted.length,
|
|
776
926
|
tables_created: tablesCreated,
|
|
777
927
|
images_processed: imagesProcessed,
|
|
778
|
-
block_ids:
|
|
779
|
-
...(
|
|
780
|
-
warning: `Skipped unsupported block types: ${
|
|
928
|
+
block_ids: allInserted.map((b: any) => b.block_id),
|
|
929
|
+
...(allSkipped.length > 0 && {
|
|
930
|
+
warning: `Skipped unsupported block types: ${allSkipped.join(", ")}.`,
|
|
781
931
|
}),
|
|
782
932
|
};
|
|
783
933
|
}
|