@customize-agent/knowledge 3.0.2 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classification/classifier.js +2 -2
- package/dist/core/change-tracker.js +5 -2
- package/dist/core/knowledge-base-manager.js +4 -3
- package/dist/embedding/embedding-provider.d.ts +17 -0
- package/dist/embedding/embedding-provider.js +66 -0
- package/dist/extraction/content-extractor.d.ts +9 -0
- package/dist/extraction/content-extractor.js +269 -85
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +8 -3
|
@@ -50,10 +50,10 @@ export class FileClassifier {
|
|
|
50
50
|
['.pdf', 'document', 'pdf'], ['.docx', 'document', 'office'], ['.doc', 'document', 'office'], ['.rtf', 'document', 'office'], ['.odt', 'document', 'office'],
|
|
51
51
|
['.pptx', 'document', 'presentation'], ['.ppt', 'document', 'presentation'], ['.odp', 'document', 'presentation'],
|
|
52
52
|
['.md', 'document', 'markdown'], ['.markdown', 'document', 'markdown'], ['.mdx', 'document', 'markdown'],
|
|
53
|
-
['.txt', 'document', 'plaintext'], ['.rst', 'document', 'plaintext'], ['.asciidoc', 'document', 'plaintext'], ['.tex', 'document', 'plaintext'],
|
|
53
|
+
['.txt', 'document', 'plaintext'], ['.textclipping', 'document', 'text_clipping'], ['.rst', 'document', 'plaintext'], ['.asciidoc', 'document', 'plaintext'], ['.tex', 'document', 'plaintext'],
|
|
54
54
|
['.epub', 'document', 'ebook'], ['.mobi', 'document', 'ebook'],
|
|
55
55
|
['.xlsx', 'spreadsheet', 'excel'], ['.xls', 'spreadsheet', 'excel'], ['.xlsm', 'spreadsheet', 'excel'],
|
|
56
|
-
['.csv', 'spreadsheet', 'csv'], ['.tsv', 'spreadsheet', '
|
|
56
|
+
['.csv', 'spreadsheet', 'csv'], ['.tsv', 'spreadsheet', 'tsv'], ['.tab', 'spreadsheet', 'tsv'], ['.ods', 'spreadsheet', 'opendoc'],
|
|
57
57
|
['.png', 'image', 'raster'], ['.jpg', 'image', 'raster'], ['.jpeg', 'image', 'raster'], ['.gif', 'image', 'raster'], ['.bmp', 'image', 'raster'], ['.webp', 'image', 'raster'], ['.tiff', 'image', 'raster'], ['.tif', 'image', 'raster'],
|
|
58
58
|
['.svg', 'image', 'vector'], ['.eps', 'image', 'vector'], ['.raw', 'image', 'raw'], ['.cr2', 'image', 'raw'], ['.nef', 'image', 'raw'], ['.dng', 'image', 'raw'],
|
|
59
59
|
['.dwg', 'cad', 'autocad'], ['.dxf', 'cad', 'autocad'], ['.dwt', 'cad', 'autocad'],
|
|
@@ -30,11 +30,14 @@ export class ChangeTracker {
|
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
32
|
const metadata = this.parseMetadata(indexed.metadataJson);
|
|
33
|
+
const extraction = metadata.extraction && typeof metadata.extraction === 'object' ? metadata.extraction : {};
|
|
34
|
+
const contentCoverage = metadata.contentCoverage ?? extraction.contentCoverage;
|
|
35
|
+
const extractionMode = metadata.extractionMode ?? extraction.extractionMode;
|
|
33
36
|
const needsReindex = indexed.status === 'error'
|
|
34
37
|
|| indexed.chunkCount === 0
|
|
35
38
|
|| (classified.format === 'pdf' && indexed.chunkCount <= 1)
|
|
36
|
-
||
|
|
37
|
-
||
|
|
39
|
+
|| contentCoverage === 'metadata_filename'
|
|
40
|
+
|| extractionMode === 'pdf_metadata_only';
|
|
38
41
|
if (needsReindex) {
|
|
39
42
|
modifiedFiles.push(classified);
|
|
40
43
|
continue;
|
|
@@ -6,7 +6,7 @@ import { FileClassifier } from '../classification/classifier.js';
|
|
|
6
6
|
import { ALL_CATEGORIES, DEFAULT_CATEGORY_DIRS, GLOBAL_KNOWLEDGE_DIR, USER_DATA_DIR } from '../constants.js';
|
|
7
7
|
import { DedupEngine } from '../dedup/dedup-engine.js';
|
|
8
8
|
import { RelationshipDetector } from '../dedup/relationship-detector.js';
|
|
9
|
-
import {
|
|
9
|
+
import { createEmbeddingProviderFromEnvironment } from '../embedding/embedding-provider.js';
|
|
10
10
|
import { ContentExtractor } from '../extraction/content-extractor.js';
|
|
11
11
|
import { FederationSearch } from '../search/federation-search.js';
|
|
12
12
|
import { CollectionManager } from '../vector/collection-manager.js';
|
|
@@ -41,7 +41,7 @@ export class KnowledgeBaseManager {
|
|
|
41
41
|
this.scope = options.scope;
|
|
42
42
|
this.projectRoot = options.projectRoot;
|
|
43
43
|
this.projectId = options.projectId;
|
|
44
|
-
this.embeddingProvider = options.embeddingProvider ??
|
|
44
|
+
this.embeddingProvider = options.embeddingProvider ?? createEmbeddingProviderFromEnvironment();
|
|
45
45
|
this.vectorStores = options.vectorStores ?? new Map();
|
|
46
46
|
this.extractor = new ContentExtractor(options.externalExtractors);
|
|
47
47
|
this.llmProvider = options.llmProvider;
|
|
@@ -200,6 +200,7 @@ export class KnowledgeBaseManager {
|
|
|
200
200
|
status: 'active',
|
|
201
201
|
metadataJson: JSON.stringify({
|
|
202
202
|
mimeType: file.mimeType,
|
|
203
|
+
...extraction.metadata,
|
|
203
204
|
extraction: extraction.metadata,
|
|
204
205
|
warnings: extraction.warnings,
|
|
205
206
|
extractionTimeMs: extraction.extractionTimeMs,
|
|
@@ -705,7 +706,7 @@ ${resultsText}
|
|
|
705
706
|
}
|
|
706
707
|
hasUsableContent(text, metadata) {
|
|
707
708
|
const coverage = String(metadata.contentCoverage ?? '');
|
|
708
|
-
if (
|
|
709
|
+
if (['metadata', 'metadata_filename', 'pdf_metadata_only', 'office_zip_empty_text', 'office_zip_failed'].includes(coverage))
|
|
709
710
|
return false;
|
|
710
711
|
return text.trim().length > 0;
|
|
711
712
|
}
|
|
@@ -14,3 +14,20 @@ export declare class HashEmbeddingProvider implements EmbeddingProvider {
|
|
|
14
14
|
private tokenize;
|
|
15
15
|
private normalize;
|
|
16
16
|
}
|
|
17
|
+
export interface OpenAICompatibleEmbeddingOptions {
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
model: string;
|
|
21
|
+
dimensions?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class OpenAICompatibleEmbeddingProvider implements EmbeddingProvider {
|
|
24
|
+
readonly model: string;
|
|
25
|
+
readonly dimensions: number;
|
|
26
|
+
private readonly baseUrl;
|
|
27
|
+
private readonly apiKey?;
|
|
28
|
+
constructor(options: OpenAICompatibleEmbeddingOptions);
|
|
29
|
+
embedDocuments(texts: string[]): Promise<number[][]>;
|
|
30
|
+
embedQuery(text: string): Promise<number[]>;
|
|
31
|
+
private embed;
|
|
32
|
+
}
|
|
33
|
+
export declare function createEmbeddingProviderFromEnvironment(): EmbeddingProvider;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
2
5
|
export class HashEmbeddingProvider {
|
|
3
6
|
dimensions;
|
|
4
7
|
model = 'hash-embedding-local';
|
|
@@ -44,3 +47,66 @@ export class HashEmbeddingProvider {
|
|
|
44
47
|
return vector.map(value => value / norm);
|
|
45
48
|
}
|
|
46
49
|
}
|
|
50
|
+
export class OpenAICompatibleEmbeddingProvider {
|
|
51
|
+
model;
|
|
52
|
+
dimensions;
|
|
53
|
+
baseUrl;
|
|
54
|
+
apiKey;
|
|
55
|
+
constructor(options) {
|
|
56
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/u, '');
|
|
57
|
+
this.apiKey = options.apiKey;
|
|
58
|
+
this.model = options.model;
|
|
59
|
+
this.dimensions = options.dimensions ?? 1024;
|
|
60
|
+
}
|
|
61
|
+
async embedDocuments(texts) {
|
|
62
|
+
return this.embed(texts);
|
|
63
|
+
}
|
|
64
|
+
async embedQuery(text) {
|
|
65
|
+
return (await this.embed([text]))[0] ?? [];
|
|
66
|
+
}
|
|
67
|
+
async embed(input) {
|
|
68
|
+
const response = await fetch(`${this.baseUrl}/embeddings`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ model: this.model, input }),
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok)
|
|
77
|
+
throw new Error(`Embedding request failed: HTTP ${response.status} ${await response.text()}`);
|
|
78
|
+
const payload = await response.json();
|
|
79
|
+
return (payload.data ?? []).map(item => item.embedding ?? []);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function readStoredEmbeddingConfig() {
|
|
83
|
+
try {
|
|
84
|
+
const configPath = path.join(os.homedir(), '.customize-agent', 'config.json');
|
|
85
|
+
if (!fs.existsSync(configPath))
|
|
86
|
+
return undefined;
|
|
87
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
88
|
+
return raw.embedding;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function createEmbeddingProviderFromEnvironment() {
|
|
95
|
+
const stored = readStoredEmbeddingConfig();
|
|
96
|
+
const provider = process.env.CUSTOMIZE_EMBEDDING_PROVIDER ?? process.env.KB_EMBEDDING_PROVIDER ?? stored?.provider;
|
|
97
|
+
if (provider === 'openai-compatible') {
|
|
98
|
+
const baseUrl = process.env.CUSTOMIZE_EMBEDDING_BASE_URL ?? process.env.KB_EMBEDDING_BASE_URL ?? stored?.baseUrl;
|
|
99
|
+
const model = process.env.CUSTOMIZE_EMBEDDING_MODEL ?? process.env.KB_EMBEDDING_MODEL ?? stored?.model;
|
|
100
|
+
if (baseUrl && model) {
|
|
101
|
+
const rawDimensions = process.env.CUSTOMIZE_EMBEDDING_DIMENSIONS ?? process.env.KB_EMBEDDING_DIMENSIONS;
|
|
102
|
+
const dimensions = Number(rawDimensions ?? stored?.dimensions ?? 1024);
|
|
103
|
+
return new OpenAICompatibleEmbeddingProvider({
|
|
104
|
+
baseUrl,
|
|
105
|
+
model,
|
|
106
|
+
apiKey: process.env.CUSTOMIZE_EMBEDDING_API_KEY ?? process.env.KB_EMBEDDING_API_KEY ?? stored?.apiKey,
|
|
107
|
+
dimensions: Number.isFinite(dimensions) ? dimensions : 1024,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return new HashEmbeddingProvider();
|
|
112
|
+
}
|
|
@@ -11,8 +11,16 @@ export declare class ContentExtractor {
|
|
|
11
11
|
constructor(externalExtractors?: ExternalExtractorRegistry);
|
|
12
12
|
extract(file: ClassifiedFile): Promise<ExtractionResult>;
|
|
13
13
|
private tryExternalExtractor;
|
|
14
|
+
private extractTextClipping;
|
|
15
|
+
private swapUtf16Bytes;
|
|
16
|
+
private extractReadableFragments;
|
|
17
|
+
private textScore;
|
|
14
18
|
private extractCad;
|
|
19
|
+
private extractDxf;
|
|
20
|
+
private tryConvertDwgToDxf;
|
|
15
21
|
private extractCadMesh;
|
|
22
|
+
private getTempRoot;
|
|
23
|
+
private extractBinaryReadableFragments;
|
|
16
24
|
private extractBinaryStrings;
|
|
17
25
|
private extractData;
|
|
18
26
|
private extractDiagram;
|
|
@@ -20,6 +28,7 @@ export declare class ContentExtractor {
|
|
|
20
28
|
private extractDelimitedText;
|
|
21
29
|
private extractOfficeDocument;
|
|
22
30
|
private extractRtf;
|
|
31
|
+
private extractLegacyWordDocument;
|
|
23
32
|
private extractLegacyOfficeBinary;
|
|
24
33
|
private extractSpreadsheet;
|
|
25
34
|
private extractOfficeZip;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
4
5
|
import { ExternalExtractorRegistry } from './external-extractor.js';
|
|
5
6
|
import { resolveAndImport, resolvePackage, getNodeModulesRoot } from './module-resolver.js';
|
|
6
7
|
export class ContentExtractor {
|
|
@@ -18,82 +19,91 @@ export class ContentExtractor {
|
|
|
18
19
|
format: file.format,
|
|
19
20
|
};
|
|
20
21
|
const external = this.tryExternalExtractor(file);
|
|
21
|
-
if (external) {
|
|
22
|
+
if (external?.text.trim()) {
|
|
22
23
|
text = external.text;
|
|
23
24
|
Object.assign(metadata, external.metadata);
|
|
24
25
|
warnings.push(...external.warnings);
|
|
25
26
|
}
|
|
26
|
-
else if (file.category === 'cad') {
|
|
27
|
-
const result = this.extractCad(file);
|
|
28
|
-
text = result.text;
|
|
29
|
-
Object.assign(metadata, result.metadata);
|
|
30
|
-
warnings.push(...result.warnings);
|
|
31
|
-
}
|
|
32
|
-
else if (file.category === 'data') {
|
|
33
|
-
const result = this.extractData(file);
|
|
34
|
-
text = result.text;
|
|
35
|
-
Object.assign(metadata, result.metadata);
|
|
36
|
-
warnings.push(...result.warnings);
|
|
37
|
-
}
|
|
38
|
-
else if (file.category === 'diagram') {
|
|
39
|
-
const result = this.extractDiagram(file);
|
|
40
|
-
text = result.text;
|
|
41
|
-
Object.assign(metadata, result.metadata);
|
|
42
|
-
warnings.push(...result.warnings);
|
|
43
|
-
}
|
|
44
|
-
else if (file.category === 'document' && file.format === 'pdf') {
|
|
45
|
-
const result = await this.extractPdf(file);
|
|
46
|
-
text = result.text;
|
|
47
|
-
Object.assign(metadata, result.metadata);
|
|
48
|
-
warnings.push(...result.warnings);
|
|
49
|
-
}
|
|
50
|
-
else if (file.category === 'document' && ['office', 'presentation'].includes(file.format)) {
|
|
51
|
-
const result = await this.extractOfficeDocument(file);
|
|
52
|
-
text = result.text;
|
|
53
|
-
Object.assign(metadata, result.metadata);
|
|
54
|
-
warnings.push(...result.warnings);
|
|
55
|
-
}
|
|
56
|
-
else if (file.category === 'spreadsheet' && ['csv', 'tsv'].includes(file.format)) {
|
|
57
|
-
const result = this.extractDelimitedText(file);
|
|
58
|
-
text = result.text;
|
|
59
|
-
Object.assign(metadata, result.metadata);
|
|
60
|
-
warnings.push(...result.warnings);
|
|
61
|
-
}
|
|
62
|
-
else if (file.category === 'spreadsheet') {
|
|
63
|
-
const result = await this.extractSpreadsheet(file);
|
|
64
|
-
text = result.text;
|
|
65
|
-
Object.assign(metadata, result.metadata);
|
|
66
|
-
warnings.push(...result.warnings);
|
|
67
|
-
}
|
|
68
|
-
else if (file.category === 'archive') {
|
|
69
|
-
const result = await this.extractArchive(file);
|
|
70
|
-
text = result.text;
|
|
71
|
-
Object.assign(metadata, result.metadata);
|
|
72
|
-
warnings.push(...result.warnings);
|
|
73
|
-
}
|
|
74
|
-
else if (file.category === 'image' && file.format !== 'vector') {
|
|
75
|
-
const result = await this.extractRasterImage(file);
|
|
76
|
-
text = result.text;
|
|
77
|
-
Object.assign(metadata, result.metadata);
|
|
78
|
-
warnings.push(...result.warnings);
|
|
79
|
-
}
|
|
80
|
-
else if (file.category === 'image' && file.format === 'vector') {
|
|
81
|
-
const result = this.extractSvg(file);
|
|
82
|
-
text = result.text;
|
|
83
|
-
Object.assign(metadata, result.metadata);
|
|
84
|
-
warnings.push(...result.warnings);
|
|
85
|
-
}
|
|
86
|
-
else if (this.isTextReadable(file)) {
|
|
87
|
-
text = fs.readFileSync(file.absolutePath, 'utf8');
|
|
88
|
-
metadata.extractionMode = 'plain_text';
|
|
89
|
-
metadata.vectorizable = true;
|
|
90
|
-
}
|
|
91
27
|
else {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
28
|
+
warnings.push(...(external?.warnings ?? []));
|
|
29
|
+
if (file.category === 'cad') {
|
|
30
|
+
const result = await this.extractCad(file);
|
|
31
|
+
text = result.text;
|
|
32
|
+
Object.assign(metadata, result.metadata);
|
|
33
|
+
warnings.push(...result.warnings);
|
|
34
|
+
}
|
|
35
|
+
else if (file.category === 'data') {
|
|
36
|
+
const result = this.extractData(file);
|
|
37
|
+
text = result.text;
|
|
38
|
+
Object.assign(metadata, result.metadata);
|
|
39
|
+
warnings.push(...result.warnings);
|
|
40
|
+
}
|
|
41
|
+
else if (file.category === 'diagram') {
|
|
42
|
+
const result = this.extractDiagram(file);
|
|
43
|
+
text = result.text;
|
|
44
|
+
Object.assign(metadata, result.metadata);
|
|
45
|
+
warnings.push(...result.warnings);
|
|
46
|
+
}
|
|
47
|
+
else if (file.category === 'document' && file.format === 'pdf') {
|
|
48
|
+
const result = await this.extractPdf(file);
|
|
49
|
+
text = result.text;
|
|
50
|
+
Object.assign(metadata, result.metadata);
|
|
51
|
+
warnings.push(...result.warnings);
|
|
52
|
+
}
|
|
53
|
+
else if (file.category === 'document' && ['office', 'presentation'].includes(file.format)) {
|
|
54
|
+
const result = await this.extractOfficeDocument(file);
|
|
55
|
+
text = result.text;
|
|
56
|
+
Object.assign(metadata, result.metadata);
|
|
57
|
+
warnings.push(...result.warnings);
|
|
58
|
+
}
|
|
59
|
+
else if (file.category === 'spreadsheet' && ['csv', 'tsv'].includes(file.format)) {
|
|
60
|
+
const result = this.extractDelimitedText(file);
|
|
61
|
+
text = result.text;
|
|
62
|
+
Object.assign(metadata, result.metadata);
|
|
63
|
+
warnings.push(...result.warnings);
|
|
64
|
+
}
|
|
65
|
+
else if (file.category === 'spreadsheet') {
|
|
66
|
+
const result = await this.extractSpreadsheet(file);
|
|
67
|
+
text = result.text;
|
|
68
|
+
Object.assign(metadata, result.metadata);
|
|
69
|
+
warnings.push(...result.warnings);
|
|
70
|
+
}
|
|
71
|
+
else if (file.category === 'archive') {
|
|
72
|
+
const result = await this.extractArchive(file);
|
|
73
|
+
text = result.text;
|
|
74
|
+
Object.assign(metadata, result.metadata);
|
|
75
|
+
warnings.push(...result.warnings);
|
|
76
|
+
}
|
|
77
|
+
else if (file.category === 'image' && file.format !== 'vector') {
|
|
78
|
+
const result = await this.extractRasterImage(file);
|
|
79
|
+
text = result.text;
|
|
80
|
+
Object.assign(metadata, result.metadata);
|
|
81
|
+
warnings.push(...result.warnings);
|
|
82
|
+
}
|
|
83
|
+
else if (file.category === 'image' && file.format === 'vector') {
|
|
84
|
+
const result = this.extractSvg(file);
|
|
85
|
+
text = result.text;
|
|
86
|
+
Object.assign(metadata, result.metadata);
|
|
87
|
+
warnings.push(...result.warnings);
|
|
88
|
+
}
|
|
89
|
+
else if (file.format === 'text_clipping') {
|
|
90
|
+
const result = this.extractTextClipping(file);
|
|
91
|
+
text = result.text;
|
|
92
|
+
Object.assign(metadata, result.metadata);
|
|
93
|
+
warnings.push(...result.warnings);
|
|
94
|
+
}
|
|
95
|
+
else if (this.isTextReadable(file)) {
|
|
96
|
+
text = fs.readFileSync(file.absolutePath, 'utf8');
|
|
97
|
+
metadata.extractionMode = 'plain_text';
|
|
98
|
+
metadata.vectorizable = true;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
text = this.metadataOnlyText(file);
|
|
102
|
+
metadata.extractionMode = 'metadata_only';
|
|
103
|
+
metadata.vectorizable = true;
|
|
104
|
+
metadata.contentCoverage = 'metadata';
|
|
105
|
+
warnings.push(`暂不支持 ${file.category}/${file.format} 内容提取,未解析出正文,未入库`);
|
|
106
|
+
}
|
|
97
107
|
}
|
|
98
108
|
return {
|
|
99
109
|
text: text.trim(),
|
|
@@ -131,12 +141,69 @@ export class ContentExtractor {
|
|
|
131
141
|
warnings.push(`外部解析器 ${extractor.name} 失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
132
142
|
}
|
|
133
143
|
}
|
|
134
|
-
return undefined;
|
|
144
|
+
return warnings.length > 0 ? { text: '', metadata: {}, warnings } : undefined;
|
|
135
145
|
}
|
|
136
|
-
|
|
146
|
+
extractTextClipping(file) {
|
|
147
|
+
const buffer = fs.readFileSync(file.absolutePath);
|
|
148
|
+
const candidates = [
|
|
149
|
+
buffer.toString('utf16le'),
|
|
150
|
+
this.swapUtf16Bytes(buffer).toString('utf16le'),
|
|
151
|
+
buffer.toString('utf8'),
|
|
152
|
+
...this.extractBinaryStrings(file.absolutePath),
|
|
153
|
+
];
|
|
154
|
+
const fragments = candidates.flatMap(candidate => this.extractReadableFragments(candidate));
|
|
155
|
+
const unique = Array.from(new Set(fragments))
|
|
156
|
+
.filter(fragment => fragment.length >= 2 && !/^bplist\d+/u.test(fragment))
|
|
157
|
+
.sort((a, b) => this.textScore(b) - this.textScore(a))
|
|
158
|
+
.slice(0, 50);
|
|
159
|
+
const text = unique.join('\n');
|
|
160
|
+
return {
|
|
161
|
+
text: text ? [this.metadataOnlyText(file), text].join('\n') : this.metadataOnlyText(file),
|
|
162
|
+
metadata: {
|
|
163
|
+
extractionMode: 'builtin_text_clipping',
|
|
164
|
+
vectorizable: true,
|
|
165
|
+
contentCoverage: text ? 'text_clipping_payload' : 'metadata',
|
|
166
|
+
fragmentCount: unique.length,
|
|
167
|
+
},
|
|
168
|
+
warnings: text ? [] : ['未从 textClipping 中提取到剪贴文本,仅入库元数据'],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
swapUtf16Bytes(buffer) {
|
|
172
|
+
const swapped = Buffer.from(buffer);
|
|
173
|
+
for (let i = 0; i + 1 < swapped.length; i += 2) {
|
|
174
|
+
const first = swapped[i] ?? 0;
|
|
175
|
+
swapped[i] = swapped[i + 1] ?? 0;
|
|
176
|
+
swapped[i + 1] = first;
|
|
177
|
+
}
|
|
178
|
+
return swapped;
|
|
179
|
+
}
|
|
180
|
+
extractReadableFragments(value) {
|
|
181
|
+
return value
|
|
182
|
+
.replace(/[^\p{L}\p{N}\p{P}\p{S}\s]/gu, '\n')
|
|
183
|
+
.split(/[\r\n]+/u)
|
|
184
|
+
.map(line => line.replace(/\s+/gu, ' ').trim())
|
|
185
|
+
.filter(line => line.length >= 2 && /[\p{L}\p{N}]/u.test(line));
|
|
186
|
+
}
|
|
187
|
+
textScore(value) {
|
|
188
|
+
const cjk = (value.match(/[\p{Script=Han}]/gu) ?? []).length;
|
|
189
|
+
const alnum = (value.match(/[\p{L}\p{N}]/gu) ?? []).length;
|
|
190
|
+
return cjk * 4 + alnum + Math.min(value.length, 200) / 20;
|
|
191
|
+
}
|
|
192
|
+
async extractCad(file) {
|
|
137
193
|
const metadata = { extractionMode: 'builtin_cad_structural', vectorizable: true };
|
|
138
194
|
const warnings = [];
|
|
139
195
|
const ext = path.extname(file.absolutePath).toLowerCase();
|
|
196
|
+
if (ext === '.dxf')
|
|
197
|
+
return await this.extractDxf(file, fs.readFileSync(file.absolutePath, 'utf8'), metadata);
|
|
198
|
+
if (ext === '.dwg') {
|
|
199
|
+
const converted = await this.tryConvertDwgToDxf(file.absolutePath);
|
|
200
|
+
if (converted?.dxfText) {
|
|
201
|
+
const parsed = await this.extractDxf(file, converted.dxfText, { ...metadata, extractionMode: converted.tool, convertedFrom: 'dwg' });
|
|
202
|
+
parsed.warnings.push(...converted.warnings);
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
warnings.push(...(converted?.warnings ?? ['未检测到可用 DWG→DXF 转换器,使用内置图纸可读文本抽取']));
|
|
206
|
+
}
|
|
140
207
|
if (file.format === 'autocad' && ext === '.dxf') {
|
|
141
208
|
const raw = fs.readFileSync(file.absolutePath, 'utf8');
|
|
142
209
|
const layers = this.matchAll(raw, /\n\s*8\s*\n([^\n]+)/gu).slice(0, 300);
|
|
@@ -213,18 +280,98 @@ export class ContentExtractor {
|
|
|
213
280
|
if (result.text.trim())
|
|
214
281
|
return result;
|
|
215
282
|
}
|
|
216
|
-
const
|
|
217
|
-
metadata.extractionMode = '
|
|
218
|
-
metadata.contentCoverage =
|
|
219
|
-
metadata.stringCount =
|
|
220
|
-
if (
|
|
221
|
-
warnings.push(`${file.format} 内置 CAD
|
|
283
|
+
const readable = this.extractBinaryReadableFragments(file.absolutePath).slice(0, 800);
|
|
284
|
+
metadata.extractionMode = 'builtin_cad_readable_fragments';
|
|
285
|
+
metadata.contentCoverage = readable.length > 0 ? 'cad_readable_text_fragments' : 'metadata';
|
|
286
|
+
metadata.stringCount = readable.length;
|
|
287
|
+
if (readable.length === 0)
|
|
288
|
+
warnings.push(`${file.format} 内置 CAD 解析器未提取到可用文本,仅记录文件元数据,未生成可检索正文切片`);
|
|
289
|
+
else
|
|
290
|
+
warnings.push(`${file.format} 未检测到专业 DWG 转换器,已使用内置可读标注/标题块抽取;如需完整图纸结构,请安装 ODA File Converter 或 LibreDWG 并配置外部解析器`);
|
|
291
|
+
return {
|
|
292
|
+
text: readable.length > 0 ? [this.metadataOnlyText(file), `CAD 图纸可读标注/标题块/属性:\n${readable.join('\n')}`].join('\n') : this.metadataOnlyText(file),
|
|
293
|
+
metadata,
|
|
294
|
+
warnings,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async extractDxf(file, raw, metadata) {
|
|
298
|
+
const warnings = [];
|
|
299
|
+
let parsed;
|
|
300
|
+
try {
|
|
301
|
+
const mod = await resolveAndImport('dxf-parser');
|
|
302
|
+
const Parser = mod.default ?? mod;
|
|
303
|
+
parsed = new Parser().parseSync(raw);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
warnings.push('dxf-parser 解析失败,已使用 DXF 文本结构抽取回退');
|
|
307
|
+
}
|
|
308
|
+
const layers = this.matchAll(raw, /\n\s*8\s*\n([^\n]+)/gu).slice(0, 300);
|
|
309
|
+
const textEntities = this.matchAll(raw, /\n\s*(?:1|3)\s*\n([^\n]+)/gu).slice(0, 800);
|
|
310
|
+
const blocks = this.matchAll(raw, /\n\s*2\s*\n([^\n]+)/gu).slice(0, 300);
|
|
311
|
+
const entityTypes = this.matchAll(raw, /\n\s*0\s*\n([A-Z][A-Z0-9_]+)/gu).slice(0, 1200);
|
|
312
|
+
const uniqueLayers = Array.from(new Set(layers));
|
|
313
|
+
const uniqueBlocks = Array.from(new Set(blocks));
|
|
314
|
+
const uniqueEntityTypes = Array.from(new Set(entityTypes));
|
|
315
|
+
metadata.layerCount = uniqueLayers.length;
|
|
316
|
+
metadata.layerNames = uniqueLayers.slice(0, 80);
|
|
317
|
+
metadata.textEntityCount = textEntities.length;
|
|
318
|
+
metadata.blockCount = uniqueBlocks.length;
|
|
319
|
+
metadata.blockNames = uniqueBlocks.slice(0, 80);
|
|
320
|
+
metadata.entityTypeCount = uniqueEntityTypes.length;
|
|
321
|
+
metadata.entityTypes = uniqueEntityTypes.slice(0, 80);
|
|
322
|
+
metadata.contentCoverage = 'dxf_layers_blocks_entities_text';
|
|
323
|
+
metadata.parsedByDxfParser = Boolean(parsed);
|
|
222
324
|
return {
|
|
223
|
-
text:
|
|
325
|
+
text: [
|
|
326
|
+
this.metadataOnlyText(file),
|
|
327
|
+
`CAD DXF 图层: ${uniqueLayers.join(', ')}`,
|
|
328
|
+
`CAD DXF 块/符号: ${uniqueBlocks.join(', ')}`,
|
|
329
|
+
`CAD DXF 实体类型: ${uniqueEntityTypes.join(', ')}`,
|
|
330
|
+
`CAD DXF 标注/文本:\n${textEntities.join('\n')}`,
|
|
331
|
+
].join('\n'),
|
|
224
332
|
metadata,
|
|
225
333
|
warnings,
|
|
226
334
|
};
|
|
227
335
|
}
|
|
336
|
+
async tryConvertDwgToDxf(filePath) {
|
|
337
|
+
const tmpDir = fs.mkdtempSync(path.join(this.getTempRoot(), 'customize-dwg-'));
|
|
338
|
+
const outputPath = path.join(tmpDir, `${path.basename(filePath, path.extname(filePath))}.dxf`);
|
|
339
|
+
try {
|
|
340
|
+
const customCmd = process.env.CUSTOMIZE_DWG_TO_DXF_CMD;
|
|
341
|
+
if (customCmd) {
|
|
342
|
+
if (/\s/u.test(customCmd))
|
|
343
|
+
return { tool: 'external_dwg_to_dxf', warnings: ['CUSTOMIZE_DWG_TO_DXF_CMD 只支持可执行文件路径;参数请使用 CUSTOMIZE_DWG_TO_DXF_ARGS JSON 数组配置'] };
|
|
344
|
+
let argTemplate = ['{input}', '{output}'];
|
|
345
|
+
try {
|
|
346
|
+
if (process.env.CUSTOMIZE_DWG_TO_DXF_ARGS)
|
|
347
|
+
argTemplate = JSON.parse(process.env.CUSTOMIZE_DWG_TO_DXF_ARGS);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return { tool: 'external_dwg_to_dxf', warnings: ['CUSTOMIZE_DWG_TO_DXF_ARGS 必须是字符串数组 JSON'] };
|
|
351
|
+
}
|
|
352
|
+
if (!Array.isArray(argTemplate) || !argTemplate.every(arg => typeof arg === 'string'))
|
|
353
|
+
return { tool: 'external_dwg_to_dxf', warnings: ['CUSTOMIZE_DWG_TO_DXF_ARGS 必须是字符串数组 JSON'] };
|
|
354
|
+
const args = argTemplate.map(arg => arg.replace(/\{input\}/gu, filePath).replace(/\{output\}/gu, outputPath));
|
|
355
|
+
const result = spawnSync(customCmd, args, { shell: false, encoding: 'utf8', timeout: 120_000 });
|
|
356
|
+
if (result.status === 0 && fs.existsSync(outputPath))
|
|
357
|
+
return { dxfText: fs.readFileSync(outputPath, 'utf8'), tool: 'external_dwg_to_dxf', warnings: [] };
|
|
358
|
+
return { tool: 'external_dwg_to_dxf', warnings: [`CUSTOMIZE_DWG_TO_DXF_CMD 转换失败: ${result.stderr || result.stdout || result.error?.message || 'unknown error'}`] };
|
|
359
|
+
}
|
|
360
|
+
const failures = [];
|
|
361
|
+
for (const bin of ['dwgread', 'dwg2dxf']) {
|
|
362
|
+
const result = spawnSync(bin, bin === 'dwgread' ? ['-O', 'DXF', '-o', outputPath, filePath] : [filePath, outputPath], { encoding: 'utf8', timeout: 120_000 });
|
|
363
|
+
if (result.status === 0 && fs.existsSync(outputPath))
|
|
364
|
+
return { dxfText: fs.readFileSync(outputPath, 'utf8'), tool: bin, warnings: [] };
|
|
365
|
+
if (result.error && 'code' in result.error && result.error.code === 'ENOENT')
|
|
366
|
+
continue;
|
|
367
|
+
failures.push(`${bin} 转换失败: ${result.stderr || result.stdout || result.error?.message || `exit ${result.status ?? 'unknown'}`}`);
|
|
368
|
+
}
|
|
369
|
+
return { tool: 'builtin_fallback', warnings: [...failures, failures.length ? 'DWG→DXF 转换失败,使用内置图纸可读文本抽取' : '未检测到可用 DWG→DXF 转换器(dwgread/dwg2dxf/CUSTOMIZE_DWG_TO_DXF_CMD),使用内置图纸可读文本抽取'] };
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
228
375
|
extractCadMesh(file, ext, metadata) {
|
|
229
376
|
const warnings = [];
|
|
230
377
|
if (['.obj', '.gltf'].includes(ext)) {
|
|
@@ -258,13 +405,25 @@ export class ContentExtractor {
|
|
|
258
405
|
metadata.contentCoverage = binaryStrings.length > 0 ? 'mesh_binary_strings' : 'metadata';
|
|
259
406
|
return { text: binaryStrings.length > 0 ? [this.metadataOnlyText(file), `Mesh 二进制字符串:\n${binaryStrings.join('\n')}`].join('\n') : '', metadata, warnings };
|
|
260
407
|
}
|
|
261
|
-
|
|
408
|
+
getTempRoot() {
|
|
409
|
+
return process.env.CUSTOMIZE_TMPDIR || process.env.TMPDIR || tmpdir();
|
|
410
|
+
}
|
|
411
|
+
extractBinaryReadableFragments(filePath) {
|
|
262
412
|
const buffer = fs.readFileSync(filePath);
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
.
|
|
413
|
+
const candidates = [
|
|
414
|
+
buffer.toString('utf8'),
|
|
415
|
+
buffer.toString('utf16le'),
|
|
416
|
+
this.swapUtf16Bytes(buffer).toString('utf16le'),
|
|
417
|
+
buffer.toString('latin1'),
|
|
418
|
+
];
|
|
419
|
+
return Array.from(new Set(candidates.flatMap(candidate => this.extractReadableFragments(candidate))))
|
|
420
|
+
.filter(value => value.length >= 3 && !/^\d+$/u.test(value))
|
|
421
|
+
.sort((a, b) => this.textScore(b) - this.textScore(a))
|
|
266
422
|
.slice(0, 2_000);
|
|
267
423
|
}
|
|
424
|
+
extractBinaryStrings(filePath) {
|
|
425
|
+
return this.extractBinaryReadableFragments(filePath);
|
|
426
|
+
}
|
|
268
427
|
extractData(file) {
|
|
269
428
|
const raw = fs.readFileSync(file.absolutePath, 'utf8');
|
|
270
429
|
const metadata = { extractionMode: 'structured_data', vectorizable: true };
|
|
@@ -381,7 +540,9 @@ export class ContentExtractor {
|
|
|
381
540
|
const ext = path.extname(file.absolutePath).toLowerCase();
|
|
382
541
|
if (ext === '.rtf')
|
|
383
542
|
return this.extractRtf(file);
|
|
384
|
-
if (ext === '.doc'
|
|
543
|
+
if (ext === '.doc')
|
|
544
|
+
return this.extractLegacyWordDocument(file);
|
|
545
|
+
if (ext === '.ppt')
|
|
385
546
|
return this.extractLegacyOfficeBinary(file);
|
|
386
547
|
if (ext === '.docx') {
|
|
387
548
|
try {
|
|
@@ -416,6 +577,29 @@ export class ContentExtractor {
|
|
|
416
577
|
warnings: text ? [] : ['RTF 解析未提取到正文,未入库'],
|
|
417
578
|
};
|
|
418
579
|
}
|
|
580
|
+
async extractLegacyWordDocument(file) {
|
|
581
|
+
const warnings = [];
|
|
582
|
+
try {
|
|
583
|
+
const mod = await resolveAndImport('word-extractor');
|
|
584
|
+
const WordExtractor = mod.default ?? mod;
|
|
585
|
+
const document = await new WordExtractor().extract(file.absolutePath);
|
|
586
|
+
const text = document.getBody().trim();
|
|
587
|
+
if (text) {
|
|
588
|
+
return {
|
|
589
|
+
text,
|
|
590
|
+
metadata: { extractionMode: 'builtin_word_extractor', vectorizable: true, contentCoverage: 'legacy_word_full_text', textLength: text.length },
|
|
591
|
+
warnings: [],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
warnings.push('word-extractor 未提取到正文,已降级为二进制可读文本抽取');
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
warnings.push(`word-extractor 解析失败,已降级为二进制可读文本抽取: ${error instanceof Error ? error.message : String(error)}`);
|
|
598
|
+
}
|
|
599
|
+
const fallback = this.extractLegacyOfficeBinary(file);
|
|
600
|
+
fallback.warnings.unshift(...warnings);
|
|
601
|
+
return fallback;
|
|
602
|
+
}
|
|
419
603
|
extractLegacyOfficeBinary(file) {
|
|
420
604
|
const strings = this.extractBinaryStrings(file.absolutePath).slice(0, 1_000);
|
|
421
605
|
const text = strings.join('\n').trim();
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { TextChunker, type ChunkConfig, type TextChunk } from './chunking/text-c
|
|
|
2
2
|
export { FileClassifier } from './classification/classifier.js';
|
|
3
3
|
export { DedupEngine, type MinHashSignature, type SimilarityMatch } from './dedup/dedup-engine.js';
|
|
4
4
|
export { RelationshipDetector } from './dedup/relationship-detector.js';
|
|
5
|
-
export { HashEmbeddingProvider, type EmbeddingProvider } from './embedding/embedding-provider.js';
|
|
5
|
+
export { HashEmbeddingProvider, OpenAICompatibleEmbeddingProvider, createEmbeddingProviderFromEnvironment, type EmbeddingProvider, type OpenAICompatibleEmbeddingOptions } from './embedding/embedding-provider.js';
|
|
6
6
|
export { ContentExtractor, type ExtractionResult } from './extraction/content-extractor.js';
|
|
7
7
|
export { CommandExternalExtractor, ExternalExtractorRegistry, type CommandExternalExtractorOptions, type ExternalExtractionResult, type ExternalExtractor, type ExternalExtractorCapability } from './extraction/external-extractor.js';
|
|
8
8
|
export { ChangeTracker } from './core/change-tracker.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { TextChunker } from './chunking/text-chunker.js';
|
|
|
4
4
|
export { FileClassifier } from './classification/classifier.js';
|
|
5
5
|
export { DedupEngine } from './dedup/dedup-engine.js';
|
|
6
6
|
export { RelationshipDetector } from './dedup/relationship-detector.js';
|
|
7
|
-
export { HashEmbeddingProvider } from './embedding/embedding-provider.js';
|
|
7
|
+
export { HashEmbeddingProvider, OpenAICompatibleEmbeddingProvider, createEmbeddingProviderFromEnvironment } from './embedding/embedding-provider.js';
|
|
8
8
|
export { ContentExtractor } from './extraction/content-extractor.js';
|
|
9
9
|
export { CommandExternalExtractor, ExternalExtractorRegistry } from './extraction/external-extractor.js';
|
|
10
10
|
export { ChangeTracker } from './core/change-tracker.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@customize-agent/knowledge",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "Local knowledge base infrastructure for customize-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"license": "MIT",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.13.0"
|
|
18
|
+
},
|
|
16
19
|
"author": "Pan-jijian",
|
|
17
20
|
"repository": {
|
|
18
21
|
"type": "git",
|
|
@@ -32,15 +35,17 @@
|
|
|
32
35
|
"knowledge-base"
|
|
33
36
|
],
|
|
34
37
|
"dependencies": {
|
|
38
|
+
"@napi-rs/canvas": "^0.1.82",
|
|
35
39
|
"better-sqlite3": "^12.10.0",
|
|
36
|
-
"
|
|
40
|
+
"dxf-parser": "^1.1.2",
|
|
37
41
|
"fast-glob": "^3.3.3",
|
|
38
42
|
"jszip": "^3.10.1",
|
|
39
|
-
"@napi-rs/canvas": "^0.1.82",
|
|
40
43
|
"mammoth": "^1.12.0",
|
|
41
44
|
"pdf-parse": "^2.4.5",
|
|
42
45
|
"pdfjs-dist": "^5.4.394",
|
|
46
|
+
"sqlite-vec": "^0.1.9",
|
|
43
47
|
"tesseract.js": "^7.0.0",
|
|
48
|
+
"word-extractor": "^1.0.4",
|
|
44
49
|
"xlsx": "^0.18.5"
|
|
45
50
|
},
|
|
46
51
|
"devDependencies": {
|