@customize-agent/knowledge 4.0.37 → 4.0.38
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.
|
@@ -28,7 +28,7 @@ export declare class TextChunker {
|
|
|
28
28
|
chunk(text: string, file: ClassifiedFile, metadata?: Record<string, unknown>): TextChunk[];
|
|
29
29
|
private createCandidates;
|
|
30
30
|
private createTextCandidates;
|
|
31
|
-
private
|
|
31
|
+
private createMixedTableCandidates;
|
|
32
32
|
private createDataCandidates;
|
|
33
33
|
private createCodeCandidates;
|
|
34
34
|
private normalizeCodeLanguage;
|
|
@@ -44,6 +44,7 @@ export declare class TextChunker {
|
|
|
44
44
|
private splitByWindow;
|
|
45
45
|
private splitBySentenceBoundary;
|
|
46
46
|
private isMarkdownTable;
|
|
47
|
+
private hasMarkdownTableBlock;
|
|
47
48
|
private extractMarkdownTableBlocks;
|
|
48
49
|
private extractMarkdownTableRowRange;
|
|
49
50
|
private splitMarkdownTable;
|
|
@@ -50,8 +50,8 @@ export class TextChunker {
|
|
|
50
50
|
return candidates.map((candidate, index) => this.createChunk(index, candidate, file, metadata));
|
|
51
51
|
}
|
|
52
52
|
createCandidates(text, file, config) {
|
|
53
|
-
if (file.category === 'spreadsheet')
|
|
54
|
-
return this.
|
|
53
|
+
if (file.category === 'spreadsheet' || this.hasMarkdownTableBlock(text))
|
|
54
|
+
return this.createMixedTableCandidates(text, file.category, config);
|
|
55
55
|
if (file.category === 'data')
|
|
56
56
|
return this.createDataCandidates(text, config);
|
|
57
57
|
if (file.category === 'code')
|
|
@@ -85,33 +85,52 @@ export class TextChunker {
|
|
|
85
85
|
});
|
|
86
86
|
return candidates;
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
88
|
+
createMixedTableCandidates(text, category, config) {
|
|
89
|
+
const tableBlocks = this.extractMarkdownTableBlocks(text);
|
|
90
|
+
if (tableBlocks.length === 0)
|
|
91
|
+
return this.createTextCandidates(text, category, config);
|
|
92
|
+
const candidates = [];
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
tableBlocks.forEach((block, parentIndex) => {
|
|
95
|
+
const beforeStart = cursor;
|
|
96
|
+
const before = text.slice(beforeStart, block.startChar).trim();
|
|
97
|
+
if (before) {
|
|
98
|
+
candidates.push(...this.createTextCandidates(before, category, config).map(candidate => ({
|
|
99
|
+
...candidate,
|
|
100
|
+
startChar: candidate.startChar + beforeStart,
|
|
101
|
+
endChar: candidate.endChar + beforeStart,
|
|
102
|
+
})));
|
|
103
|
+
}
|
|
104
|
+
const sectionTitle = this.extractSectionTitle(block.text) ?? '表格数据';
|
|
105
|
+
for (const [childIndex, part] of this.splitMarkdownTable(block.text, config.maxChunkSize).entries()) {
|
|
106
|
+
const localStart = Math.max(0, block.text.indexOf(part.slice(0, 40)));
|
|
107
|
+
const startChar = block.startChar + localStart;
|
|
108
|
+
candidates.push({
|
|
109
|
+
text: part,
|
|
110
|
+
startChar,
|
|
111
|
+
endChar: startChar + part.length,
|
|
112
|
+
sectionTitle,
|
|
113
|
+
titlePath: sectionTitle,
|
|
114
|
+
kind: 'table',
|
|
115
|
+
parentId: `table-${parentIndex}`,
|
|
116
|
+
parentIndex,
|
|
117
|
+
childIndex,
|
|
118
|
+
rowRange: this.extractMarkdownTableRowRange(part),
|
|
119
|
+
parentText: block.text,
|
|
111
120
|
});
|
|
112
121
|
}
|
|
122
|
+
cursor = block.startChar + block.text.length;
|
|
123
|
+
});
|
|
124
|
+
const afterStart = cursor;
|
|
125
|
+
const after = text.slice(afterStart).trim();
|
|
126
|
+
if (after) {
|
|
127
|
+
candidates.push(...this.createTextCandidates(after, category, config).map(candidate => ({
|
|
128
|
+
...candidate,
|
|
129
|
+
startChar: candidate.startChar + afterStart,
|
|
130
|
+
endChar: candidate.endChar + afterStart,
|
|
131
|
+
})));
|
|
113
132
|
}
|
|
114
|
-
return
|
|
133
|
+
return candidates;
|
|
115
134
|
}
|
|
116
135
|
createDataCandidates(text, config) {
|
|
117
136
|
const sections = text.split(/\n(?=[\w.[\]-]+[::]\s)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
|
|
@@ -331,6 +350,9 @@ export class TextChunker {
|
|
|
331
350
|
const lines = text.trim().split(/\r?\n/u);
|
|
332
351
|
return lines.length >= 3 && lines.some(line => /^\s*\|?\s*:?-{3,}:?\s*\|/u.test(line));
|
|
333
352
|
}
|
|
353
|
+
hasMarkdownTableBlock(text) {
|
|
354
|
+
return /^\s*\|?\s*:?-{3,}:?\s*\|/mu.test(text);
|
|
355
|
+
}
|
|
334
356
|
extractMarkdownTableBlocks(text) {
|
|
335
357
|
const lines = text.split(/\r?\n/u);
|
|
336
358
|
const lineStarts = [];
|
|
@@ -41,9 +41,15 @@ export declare class ContentExtractor {
|
|
|
41
41
|
private extractRtf;
|
|
42
42
|
private extractLegacyWordDocument;
|
|
43
43
|
private extractLegacyOfficeBinary;
|
|
44
|
+
private extractLegacyOfficeBinaryWithWarnings;
|
|
45
|
+
private isZipOpenXmlFile;
|
|
46
|
+
private isOleCompoundFile;
|
|
47
|
+
private extractOleOfficeDocument;
|
|
44
48
|
private findMergedCellValue;
|
|
45
49
|
private extractSpreadsheet;
|
|
46
50
|
private extractDocxStyleTreeMarkdown;
|
|
51
|
+
private extractDocxTableRows;
|
|
52
|
+
private extractDocxCellText;
|
|
47
53
|
private docxHeadingLevel;
|
|
48
54
|
private extractOfficeZip;
|
|
49
55
|
private extractRasterImage;
|
|
@@ -653,6 +653,10 @@ export class ContentExtractor {
|
|
|
653
653
|
}
|
|
654
654
|
async extractOfficeDocument(file) {
|
|
655
655
|
const ext = path.extname(file.absolutePath).toLowerCase();
|
|
656
|
+
const isZip = this.isZipOpenXmlFile(file.absolutePath);
|
|
657
|
+
const isOle = this.isOleCompoundFile(file.absolutePath);
|
|
658
|
+
if (isOle)
|
|
659
|
+
return this.extractOleOfficeDocument(file, ext === '.docx' ? ['文件扩展名为 .docx,但真实格式为旧版 OLE/CFB Office 复合文档,已按旧版 Office 解析'] : []);
|
|
656
660
|
if (ext === '.rtf')
|
|
657
661
|
return this.extractRtf(file);
|
|
658
662
|
if (ext === '.doc')
|
|
@@ -660,6 +664,8 @@ export class ContentExtractor {
|
|
|
660
664
|
if (ext === '.ppt')
|
|
661
665
|
return this.extractLegacyOfficeBinary(file);
|
|
662
666
|
if (ext === '.docx') {
|
|
667
|
+
if (!isZip)
|
|
668
|
+
return this.extractLegacyOfficeBinaryWithWarnings(file, ['文件扩展名为 .docx,但未检测到 OpenXML ZIP 文件头,已降级为二进制可读文本抽取']);
|
|
663
669
|
try {
|
|
664
670
|
const styledMarkdown = await this.extractDocxStyleTreeMarkdown(file.absolutePath);
|
|
665
671
|
if (styledMarkdown.trim()) {
|
|
@@ -732,6 +738,37 @@ export class ContentExtractor {
|
|
|
732
738
|
warnings: text ? [] : ['旧版 Office 二进制文件未提取到正文,未入库'],
|
|
733
739
|
};
|
|
734
740
|
}
|
|
741
|
+
extractLegacyOfficeBinaryWithWarnings(file, warnings) {
|
|
742
|
+
const result = this.extractLegacyOfficeBinary(file);
|
|
743
|
+
return { ...result, warnings: [...warnings, ...result.warnings] };
|
|
744
|
+
}
|
|
745
|
+
isZipOpenXmlFile(filePath) {
|
|
746
|
+
const header = fs.readFileSync(filePath).subarray(0, 4);
|
|
747
|
+
return header.length >= 4 && header[0] === 0x50 && header[1] === 0x4b && [0x03, 0x05, 0x07].includes(header[2] ?? -1);
|
|
748
|
+
}
|
|
749
|
+
isOleCompoundFile(filePath) {
|
|
750
|
+
const signature = fs.readFileSync(filePath).subarray(0, 8);
|
|
751
|
+
return signature.equals(Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]));
|
|
752
|
+
}
|
|
753
|
+
async extractOleOfficeDocument(file, warnings = []) {
|
|
754
|
+
const word = await this.extractLegacyWordDocument(file);
|
|
755
|
+
if (word.text.trim() && word.metadata.extractionMode === 'builtin_word_extractor') {
|
|
756
|
+
return {
|
|
757
|
+
text: word.text,
|
|
758
|
+
metadata: { ...word.metadata, realOfficeContainer: 'ole_cfb' },
|
|
759
|
+
warnings: [...warnings, ...word.warnings],
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const spreadsheet = await this.extractSpreadsheet(file);
|
|
763
|
+
if (spreadsheet.text.trim() && spreadsheet.metadata.extractionMode !== 'office_zip_failed') {
|
|
764
|
+
return {
|
|
765
|
+
text: spreadsheet.text,
|
|
766
|
+
metadata: { ...spreadsheet.metadata, realOfficeContainer: 'ole_cfb' },
|
|
767
|
+
warnings: [...warnings, ...spreadsheet.warnings],
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return this.extractLegacyOfficeBinaryWithWarnings(file, warnings);
|
|
771
|
+
}
|
|
735
772
|
findMergedCellValue(sheet, merges, row, col, XLSX) {
|
|
736
773
|
const merge = merges.find(item => row >= item.s.r && row <= item.e.r && col >= item.s.c && col <= item.e.c);
|
|
737
774
|
if (!merge)
|
|
@@ -814,6 +851,7 @@ export class ContentExtractor {
|
|
|
814
851
|
// 在 xml 中 w:body 的一级子节点通常是 w:p 和 w:tbl
|
|
815
852
|
const elements = Array.from(docXml.matchAll(/<(w:p|w:tbl)[\s>][\s\S]*?<\/\1>/gu), match => ({ tag: match[1], xml: match[0] }));
|
|
816
853
|
const lines = [];
|
|
854
|
+
let tableIndex = 0;
|
|
817
855
|
for (const { tag, xml } of elements) {
|
|
818
856
|
if (tag === 'w:p') {
|
|
819
857
|
const texts = Array.from(xml.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/gu), match => this.stripXml(match[1] ?? '')).join('');
|
|
@@ -826,34 +864,58 @@ export class ContentExtractor {
|
|
|
826
864
|
lines.push(`${level > 0 ? `${'#'.repeat(level)} ` : ''}${texts.trim()}`);
|
|
827
865
|
}
|
|
828
866
|
else if (tag === 'w:tbl') {
|
|
829
|
-
|
|
830
|
-
const rows =
|
|
831
|
-
for (const tr of trList) {
|
|
832
|
-
const tcList = Array.from(tr.matchAll(/<w:tc[\s>][\s\S]*?<\/w:tc>/gu), match => match[0]);
|
|
833
|
-
const cols = [];
|
|
834
|
-
for (const tc of tcList) {
|
|
835
|
-
const tcText = Array.from(tc.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/gu), match => this.stripXml(match[1] ?? '')).join('');
|
|
836
|
-
cols.push(tcText.trim().replace(/\r?\n/gu, ' '));
|
|
837
|
-
}
|
|
838
|
-
rows.push(cols);
|
|
839
|
-
}
|
|
867
|
+
tableIndex += 1;
|
|
868
|
+
const rows = this.extractDocxTableRows(xml);
|
|
840
869
|
if (rows.length > 0) {
|
|
841
|
-
const maxCols = Math.max(...rows.map(
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
r.push(''); });
|
|
870
|
+
const maxCols = Math.max(...rows.map(row => row.length), 1);
|
|
871
|
+
rows.forEach(row => { while (row.length < maxCols)
|
|
872
|
+
row.push(''); });
|
|
845
873
|
const header = rows[0] || Array(maxCols).fill('');
|
|
846
|
-
const mdTable =
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
lines.push(mdTable);
|
|
874
|
+
const mdTable = this.toMarkdownTable(header, rows.slice(1));
|
|
875
|
+
const declarations = rows.slice(1).flatMap((row, rowIndex) => row.map((value, colIndex) => {
|
|
876
|
+
const column = header[colIndex] || `COL${colIndex + 1}`;
|
|
877
|
+
return `表${tableIndex}.R${rowIndex + 2}C${colIndex + 1} ${column}: ${value}`;
|
|
878
|
+
})).filter(line => !line.endsWith(': '));
|
|
879
|
+
lines.push([`DOCX 表格 ${tableIndex}`, mdTable, '表格路径声明', ...declarations].join('\n'));
|
|
852
880
|
}
|
|
853
881
|
}
|
|
854
882
|
}
|
|
855
883
|
return lines.join('\n\n');
|
|
856
884
|
}
|
|
885
|
+
extractDocxTableRows(tableXml) {
|
|
886
|
+
const activeVMerges = new Map();
|
|
887
|
+
return Array.from(tableXml.matchAll(/<w:tr[\s>][\s\S]*?<\/w:tr>/gu), match => match[0]).map(tr => {
|
|
888
|
+
const row = [];
|
|
889
|
+
for (const tc of Array.from(tr.matchAll(/<w:tc[\s>][\s\S]*?<\/w:tc>/gu), match => match[0])) {
|
|
890
|
+
const gridSpan = Math.max(1, Number(/<w:gridSpan\s+w:val="(\d+)"/u.exec(tc)?.[1] ?? 1));
|
|
891
|
+
const vMerge = /<w:vMerge(?:\s+w:val="([^"]+)")?\s*\/?/u.exec(tc)?.[1] ?? (/<w:vMerge\b/u.test(tc) ? 'continue' : undefined);
|
|
892
|
+
const cellText = this.extractDocxCellText(tc);
|
|
893
|
+
const colIndex = row.length;
|
|
894
|
+
const value = vMerge === 'continue' ? (activeVMerges.get(colIndex) ?? cellText) : cellText;
|
|
895
|
+
if (vMerge === 'restart' || (vMerge && cellText))
|
|
896
|
+
activeVMerges.set(colIndex, cellText);
|
|
897
|
+
if (!vMerge)
|
|
898
|
+
activeVMerges.delete(colIndex);
|
|
899
|
+
for (let index = 0; index < gridSpan; index++)
|
|
900
|
+
row.push(index === 0 ? value : '');
|
|
901
|
+
}
|
|
902
|
+
return row;
|
|
903
|
+
}).filter(row => row.some(Boolean));
|
|
904
|
+
}
|
|
905
|
+
extractDocxCellText(cellXml) {
|
|
906
|
+
return Array.from(cellXml.matchAll(/<w:p[\s>][\s\S]*?<\/w:p>/gu), match => match[0])
|
|
907
|
+
.map(paragraph => Array.from(paragraph.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>|<w:tab\s*\/>|<w:br\s*\/>/gu), match => {
|
|
908
|
+
if (match[1] != null)
|
|
909
|
+
return this.stripXml(match[1]);
|
|
910
|
+
if (match[0].startsWith('<w:tab'))
|
|
911
|
+
return ' ';
|
|
912
|
+
return '\n';
|
|
913
|
+
}).join('').trim())
|
|
914
|
+
.filter(Boolean)
|
|
915
|
+
.join(' / ')
|
|
916
|
+
.replace(/\s+/gu, ' ')
|
|
917
|
+
.trim();
|
|
918
|
+
}
|
|
857
919
|
docxHeadingLevel(style, bold, size, text) {
|
|
858
920
|
const normalized = style.toLowerCase();
|
|
859
921
|
const heading = /heading(\d)|标题(\d)|h(\d)/iu.exec(normalized);
|