@customize-agent/knowledge 3.0.2 → 3.0.3

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.
@@ -50,7 +50,7 @@ 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
56
  ['.csv', 'spreadsheet', 'csv'], ['.tsv', 'spreadsheet', 'csv'], ['.tab', 'spreadsheet', 'csv'], ['.ods', 'spreadsheet', 'opendoc'],
@@ -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 {
@@ -24,7 +25,7 @@ export class ContentExtractor {
24
25
  warnings.push(...external.warnings);
25
26
  }
26
27
  else if (file.category === 'cad') {
27
- const result = this.extractCad(file);
28
+ const result = await this.extractCad(file);
28
29
  text = result.text;
29
30
  Object.assign(metadata, result.metadata);
30
31
  warnings.push(...result.warnings);
@@ -83,6 +84,12 @@ export class ContentExtractor {
83
84
  Object.assign(metadata, result.metadata);
84
85
  warnings.push(...result.warnings);
85
86
  }
87
+ else if (file.format === 'text_clipping') {
88
+ const result = this.extractTextClipping(file);
89
+ text = result.text;
90
+ Object.assign(metadata, result.metadata);
91
+ warnings.push(...result.warnings);
92
+ }
86
93
  else if (this.isTextReadable(file)) {
87
94
  text = fs.readFileSync(file.absolutePath, 'utf8');
88
95
  metadata.extractionMode = 'plain_text';
@@ -133,10 +140,67 @@ export class ContentExtractor {
133
140
  }
134
141
  return undefined;
135
142
  }
136
- extractCad(file) {
143
+ extractTextClipping(file) {
144
+ const buffer = fs.readFileSync(file.absolutePath);
145
+ const candidates = [
146
+ buffer.toString('utf16le'),
147
+ this.swapUtf16Bytes(buffer).toString('utf16le'),
148
+ buffer.toString('utf8'),
149
+ ...this.extractBinaryStrings(file.absolutePath),
150
+ ];
151
+ const fragments = candidates.flatMap(candidate => this.extractReadableFragments(candidate));
152
+ const unique = Array.from(new Set(fragments))
153
+ .filter(fragment => fragment.length >= 2 && !/^bplist\d+/u.test(fragment))
154
+ .sort((a, b) => this.textScore(b) - this.textScore(a))
155
+ .slice(0, 50);
156
+ const text = unique.join('\n');
157
+ return {
158
+ text: text ? [this.metadataOnlyText(file), text].join('\n') : this.metadataOnlyText(file),
159
+ metadata: {
160
+ extractionMode: 'builtin_text_clipping',
161
+ vectorizable: true,
162
+ contentCoverage: text ? 'text_clipping_payload' : 'metadata',
163
+ fragmentCount: unique.length,
164
+ },
165
+ warnings: text ? [] : ['未从 textClipping 中提取到剪贴文本,仅入库元数据'],
166
+ };
167
+ }
168
+ swapUtf16Bytes(buffer) {
169
+ const swapped = Buffer.from(buffer);
170
+ for (let i = 0; i + 1 < swapped.length; i += 2) {
171
+ const first = swapped[i] ?? 0;
172
+ swapped[i] = swapped[i + 1] ?? 0;
173
+ swapped[i + 1] = first;
174
+ }
175
+ return swapped;
176
+ }
177
+ extractReadableFragments(value) {
178
+ return value
179
+ .replace(/[^\p{L}\p{N}\p{P}\p{S}\s]/gu, '\n')
180
+ .split(/[\r\n]+/u)
181
+ .map(line => line.replace(/\s+/gu, ' ').trim())
182
+ .filter(line => line.length >= 2 && /[\p{L}\p{N}]/u.test(line));
183
+ }
184
+ textScore(value) {
185
+ const cjk = (value.match(/[\p{Script=Han}]/gu) ?? []).length;
186
+ const alnum = (value.match(/[\p{L}\p{N}]/gu) ?? []).length;
187
+ return cjk * 4 + alnum + Math.min(value.length, 200) / 20;
188
+ }
189
+ async extractCad(file) {
137
190
  const metadata = { extractionMode: 'builtin_cad_structural', vectorizable: true };
138
191
  const warnings = [];
139
192
  const ext = path.extname(file.absolutePath).toLowerCase();
193
+ if (ext === '.dxf')
194
+ return await this.extractDxf(file, fs.readFileSync(file.absolutePath, 'utf8'), metadata);
195
+ if (ext === '.dwg') {
196
+ const converted = await this.tryConvertDwgToDxf(file.absolutePath);
197
+ if (converted?.dxfText) {
198
+ const parsed = await this.extractDxf(file, converted.dxfText, { ...metadata, extractionMode: converted.tool, convertedFrom: 'dwg' });
199
+ parsed.warnings.push(...converted.warnings);
200
+ return parsed;
201
+ }
202
+ warnings.push(...(converted?.warnings ?? ['未检测到可用 DWG→DXF 转换器,使用内置图纸可读文本抽取']));
203
+ }
140
204
  if (file.format === 'autocad' && ext === '.dxf') {
141
205
  const raw = fs.readFileSync(file.absolutePath, 'utf8');
142
206
  const layers = this.matchAll(raw, /\n\s*8\s*\n([^\n]+)/gu).slice(0, 300);
@@ -213,18 +277,82 @@ export class ContentExtractor {
213
277
  if (result.text.trim())
214
278
  return result;
215
279
  }
216
- const binaryStrings = this.extractBinaryStrings(file.absolutePath).slice(0, 500);
217
- metadata.extractionMode = 'builtin_cad_binary_strings';
218
- metadata.contentCoverage = binaryStrings.length > 0 ? 'cad_binary_strings' : 'metadata';
219
- metadata.stringCount = binaryStrings.length;
220
- if (binaryStrings.length === 0)
221
- warnings.push(`${file.format} 内置 CAD 解析器未提取到可用文本,未入库`);
280
+ const readable = this.extractBinaryReadableFragments(file.absolutePath).slice(0, 800);
281
+ metadata.extractionMode = 'builtin_cad_readable_fragments';
282
+ metadata.contentCoverage = readable.length > 0 ? 'cad_readable_text_fragments' : 'metadata';
283
+ metadata.stringCount = readable.length;
284
+ if (readable.length === 0)
285
+ warnings.push(`${file.format} 内置 CAD 解析器未提取到可用文本,已入库文件元数据用于检索`);
286
+ else
287
+ warnings.push(`${file.format} 未检测到专业 DWG 转换器,已使用内置可读标注/标题块抽取;如需完整图纸结构,请安装 ODA File Converter 或 LibreDWG 并配置外部解析器`);
222
288
  return {
223
- text: binaryStrings.length > 0 ? [this.metadataOnlyText(file), `CAD 二进制字符串/标题块:\n${binaryStrings.join('\n')}`].join('\n') : '',
289
+ text: readable.length > 0 ? [this.metadataOnlyText(file), `CAD 图纸可读标注/标题块/属性:\n${readable.join('\n')}`].join('\n') : this.metadataOnlyText(file),
224
290
  metadata,
225
291
  warnings,
226
292
  };
227
293
  }
294
+ async extractDxf(file, raw, metadata) {
295
+ const warnings = [];
296
+ let parsed;
297
+ try {
298
+ const mod = await resolveAndImport('dxf-parser');
299
+ const Parser = mod.default ?? mod;
300
+ parsed = new Parser().parseSync(raw);
301
+ }
302
+ catch {
303
+ warnings.push('dxf-parser 解析失败,已使用 DXF 文本结构抽取回退');
304
+ }
305
+ const layers = this.matchAll(raw, /\n\s*8\s*\n([^\n]+)/gu).slice(0, 300);
306
+ const textEntities = this.matchAll(raw, /\n\s*(?:1|3)\s*\n([^\n]+)/gu).slice(0, 800);
307
+ const blocks = this.matchAll(raw, /\n\s*2\s*\n([^\n]+)/gu).slice(0, 300);
308
+ const entityTypes = this.matchAll(raw, /\n\s*0\s*\n([A-Z][A-Z0-9_]+)/gu).slice(0, 1200);
309
+ const uniqueLayers = Array.from(new Set(layers));
310
+ const uniqueBlocks = Array.from(new Set(blocks));
311
+ const uniqueEntityTypes = Array.from(new Set(entityTypes));
312
+ metadata.layerCount = uniqueLayers.length;
313
+ metadata.layerNames = uniqueLayers.slice(0, 80);
314
+ metadata.textEntityCount = textEntities.length;
315
+ metadata.blockCount = uniqueBlocks.length;
316
+ metadata.blockNames = uniqueBlocks.slice(0, 80);
317
+ metadata.entityTypeCount = uniqueEntityTypes.length;
318
+ metadata.entityTypes = uniqueEntityTypes.slice(0, 80);
319
+ metadata.contentCoverage = 'dxf_layers_blocks_entities_text';
320
+ metadata.parsedByDxfParser = Boolean(parsed);
321
+ return {
322
+ text: [
323
+ this.metadataOnlyText(file),
324
+ `CAD DXF 图层: ${uniqueLayers.join(', ')}`,
325
+ `CAD DXF 块/符号: ${uniqueBlocks.join(', ')}`,
326
+ `CAD DXF 实体类型: ${uniqueEntityTypes.join(', ')}`,
327
+ `CAD DXF 标注/文本:\n${textEntities.join('\n')}`,
328
+ ].join('\n'),
329
+ metadata,
330
+ warnings,
331
+ };
332
+ }
333
+ async tryConvertDwgToDxf(filePath) {
334
+ const tmpDir = fs.mkdtempSync(path.join(this.getTempRoot(), 'customize-dwg-'));
335
+ const outputPath = path.join(tmpDir, `${path.basename(filePath, path.extname(filePath))}.dxf`);
336
+ try {
337
+ const customCmd = process.env.CUSTOMIZE_DWG_TO_DXF_CMD;
338
+ if (customCmd) {
339
+ const command = customCmd.replace(/\{input\}/gu, filePath).replace(/\{output\}/gu, outputPath);
340
+ const result = spawnSync(command, { shell: true, encoding: 'utf8', timeout: 120_000 });
341
+ if (result.status === 0 && fs.existsSync(outputPath))
342
+ return { dxfText: fs.readFileSync(outputPath, 'utf8'), tool: 'external_dwg_to_dxf', warnings: [] };
343
+ return { tool: 'external_dwg_to_dxf', warnings: [`CUSTOMIZE_DWG_TO_DXF_CMD 转换失败: ${result.stderr || result.stdout || result.error?.message || 'unknown error'}`] };
344
+ }
345
+ for (const bin of ['dwgread', 'dwg2dxf']) {
346
+ const result = spawnSync(bin, bin === 'dwgread' ? ['-O', 'DXF', '-o', outputPath, filePath] : [filePath, outputPath], { encoding: 'utf8', timeout: 120_000 });
347
+ if (result.status === 0 && fs.existsSync(outputPath))
348
+ return { dxfText: fs.readFileSync(outputPath, 'utf8'), tool: bin, warnings: [] };
349
+ }
350
+ return { tool: 'builtin_fallback', warnings: ['未检测到可用 DWG→DXF 转换器(dwgread/dwg2dxf/CUSTOMIZE_DWG_TO_DXF_CMD),使用内置图纸可读文本抽取'] };
351
+ }
352
+ finally {
353
+ fs.rmSync(tmpDir, { recursive: true, force: true });
354
+ }
355
+ }
228
356
  extractCadMesh(file, ext, metadata) {
229
357
  const warnings = [];
230
358
  if (['.obj', '.gltf'].includes(ext)) {
@@ -258,13 +386,25 @@ export class ContentExtractor {
258
386
  metadata.contentCoverage = binaryStrings.length > 0 ? 'mesh_binary_strings' : 'metadata';
259
387
  return { text: binaryStrings.length > 0 ? [this.metadataOnlyText(file), `Mesh 二进制字符串:\n${binaryStrings.join('\n')}`].join('\n') : '', metadata, warnings };
260
388
  }
261
- extractBinaryStrings(filePath) {
389
+ getTempRoot() {
390
+ return process.env.CUSTOMIZE_TMPDIR || process.env.TMPDIR || tmpdir();
391
+ }
392
+ extractBinaryReadableFragments(filePath) {
262
393
  const buffer = fs.readFileSync(filePath);
263
- const raw = buffer.toString('latin1');
264
- return Array.from(raw.matchAll(/[A-Za-z0-9_ .:\-/\\\u4e00-\u9fa5]{4,}/gu), match => match[0].trim())
265
- .filter(value => value.length >= 4 && !/^\d+$/u.test(value))
394
+ const candidates = [
395
+ buffer.toString('utf8'),
396
+ buffer.toString('utf16le'),
397
+ this.swapUtf16Bytes(buffer).toString('utf16le'),
398
+ buffer.toString('latin1'),
399
+ ];
400
+ return Array.from(new Set(candidates.flatMap(candidate => this.extractReadableFragments(candidate))))
401
+ .filter(value => value.length >= 3 && !/^\d+$/u.test(value))
402
+ .sort((a, b) => this.textScore(b) - this.textScore(a))
266
403
  .slice(0, 2_000);
267
404
  }
405
+ extractBinaryStrings(filePath) {
406
+ return this.extractBinaryReadableFragments(filePath);
407
+ }
268
408
  extractData(file) {
269
409
  const raw = fs.readFileSync(file.absolutePath, 'utf8');
270
410
  const metadata = { extractionMode: 'structured_data', vectorizable: true };
@@ -381,7 +521,9 @@ export class ContentExtractor {
381
521
  const ext = path.extname(file.absolutePath).toLowerCase();
382
522
  if (ext === '.rtf')
383
523
  return this.extractRtf(file);
384
- if (ext === '.doc' || ext === '.ppt')
524
+ if (ext === '.doc')
525
+ return this.extractLegacyWordDocument(file);
526
+ if (ext === '.ppt')
385
527
  return this.extractLegacyOfficeBinary(file);
386
528
  if (ext === '.docx') {
387
529
  try {
@@ -416,6 +558,25 @@ export class ContentExtractor {
416
558
  warnings: text ? [] : ['RTF 解析未提取到正文,未入库'],
417
559
  };
418
560
  }
561
+ async extractLegacyWordDocument(file) {
562
+ try {
563
+ const mod = await resolveAndImport('word-extractor');
564
+ const WordExtractor = mod.default ?? mod;
565
+ const document = await new WordExtractor().extract(file.absolutePath);
566
+ const text = document.getBody().trim();
567
+ if (text) {
568
+ return {
569
+ text,
570
+ metadata: { extractionMode: 'builtin_word_extractor', vectorizable: true, contentCoverage: 'legacy_word_full_text', textLength: text.length },
571
+ warnings: [],
572
+ };
573
+ }
574
+ }
575
+ catch {
576
+ // fallback below
577
+ }
578
+ return this.extractLegacyOfficeBinary(file);
579
+ }
419
580
  extractLegacyOfficeBinary(file) {
420
581
  const strings = this.extractBinaryStrings(file.absolutePath).slice(0, 1_000);
421
582
  const text = strings.join('\n').trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,15 +32,17 @@
32
32
  "knowledge-base"
33
33
  ],
34
34
  "dependencies": {
35
+ "@napi-rs/canvas": "^0.1.82",
35
36
  "better-sqlite3": "^12.10.0",
36
- "sqlite-vec": "^0.1.9",
37
+ "dxf-parser": "^1.1.2",
37
38
  "fast-glob": "^3.3.3",
38
39
  "jszip": "^3.10.1",
39
- "@napi-rs/canvas": "^0.1.82",
40
40
  "mammoth": "^1.12.0",
41
41
  "pdf-parse": "^2.4.5",
42
42
  "pdfjs-dist": "^5.4.394",
43
+ "sqlite-vec": "^0.1.9",
43
44
  "tesseract.js": "^7.0.0",
45
+ "word-extractor": "^1.0.4",
44
46
  "xlsx": "^0.18.5"
45
47
  },
46
48
  "devDependencies": {