@customize-agent/knowledge 4.0.26 → 4.0.28

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.
@@ -49,6 +49,9 @@ export declare class TesseractJsProvider implements OcrProvider {
49
49
  filePath?: string;
50
50
  }): Promise<OcrResult>;
51
51
  getWarnings(): string[];
52
+ private pushWarning;
53
+ private recognizeWithTimeout;
54
+ private resetWorker;
52
55
  private readImageDimensions;
53
56
  private isTooSmallForOcr;
54
57
  private getWorker;
@@ -11,17 +11,12 @@ import { fileURLToPath } from 'node:url';
11
11
  import { resolveAndImport, resolvePackage } from './module-resolver.js';
12
12
  // ─── 路径工具 ───────────────────────────────────────────────────
13
13
  const knowledgeDir = path.dirname(fileURLToPath(import.meta.url));
14
+ const OCR_NOISE_SUPPRESSION_KEY = Symbol.for('customize-agent.ocr-noise-suppression');
14
15
  const OCR_NATIVE_NOISE_PATTERNS = [
15
- /Image too small to scale/u,
16
- /Line cannot be recognized/u,
17
- /empty image/iu
16
+ /^Image too small to scale!!(?:\s*\([^)]*\))?$/u,
17
+ /^Line cannot be recognized!!$/u,
18
+ /^empty image$/iu,
18
19
  ];
19
- const OCR_NOISE_SUPPRESSION_KEY = Symbol.for('customize-agent.ocr-noise-suppression');
20
- function ocrNoiseSuppressionState() {
21
- const globalState = globalThis;
22
- globalState[OCR_NOISE_SUPPRESSION_KEY] ??= { depth: 0 };
23
- return globalState[OCR_NOISE_SUPPRESSION_KEY];
24
- }
25
20
  function tessdataDir() {
26
21
  if (process.env.TESSDATA_PREFIX)
27
22
  return process.env.TESSDATA_PREFIX;
@@ -30,78 +25,61 @@ function tessdataDir() {
30
25
  return pkg;
31
26
  return pkg;
32
27
  }
33
- function isNativeOcrNoise(chunk) {
34
- const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk instanceof Uint8Array ? Buffer.from(chunk).toString('utf8') : typeof chunk === 'string' ? chunk : '';
35
- if (!text)
36
- return false;
37
- return OCR_NATIVE_NOISE_PATTERNS.some(pattern => pattern.test(text));
28
+ function ocrNoiseSuppressionState() {
29
+ const globalState = globalThis;
30
+ globalState[OCR_NOISE_SUPPRESSION_KEY] ??= {};
31
+ return globalState[OCR_NOISE_SUPPRESSION_KEY];
32
+ }
33
+ function textFromChunk(chunk) {
34
+ return Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk instanceof Uint8Array ? Buffer.from(chunk).toString('utf8') : typeof chunk === 'string' ? chunk : '';
35
+ }
36
+ function isOcrNoiseLine(line) {
37
+ const text = line.trim();
38
+ return !!text && OCR_NATIVE_NOISE_PATTERNS.some(pattern => pattern.test(text));
38
39
  }
39
- async function suppressNativeOcrNoise(operation) {
40
+ function filterNativeOcrNoiseText(text) {
41
+ const hasTrailingNewline = /\r?\n$/u.test(text);
42
+ const lines = text.split(/\r?\n/u);
43
+ const nonEmptyLines = lines.filter(line => line.trim());
44
+ if (nonEmptyLines.length > 0 && nonEmptyLines.every(isOcrNoiseLine))
45
+ return '';
46
+ const kept = lines.filter(line => !isOcrNoiseLine(line));
47
+ return kept.join('\n') + (hasTrailingNewline && kept.length > 0 ? '\n' : '');
48
+ }
49
+ function ensureOcrNoiseSuppressed() {
40
50
  const state = ocrNoiseSuppressionState();
41
- if (state.depth === 0) {
42
- state.stdout = process.stdout.write;
43
- state.stderr = process.stderr.write;
44
- const filter = (original) => function write(chunk, ...args) {
45
- if (isNativeOcrNoise(chunk)) {
46
- const callback = args.find((arg) => typeof arg === 'function');
47
- if (callback)
48
- process.nextTick(callback);
49
- return true;
50
- }
51
+ if (state.installed)
52
+ return;
53
+ state.installed = true;
54
+ state.stdout = process.stdout.write;
55
+ state.stderr = process.stderr.write;
56
+ state.log = console.log;
57
+ state.warn = console.warn;
58
+ state.error = console.error;
59
+ const filterWrite = (original) => function write(chunk, ...args) {
60
+ const text = textFromChunk(chunk);
61
+ if (!text)
51
62
  return original.call(this, chunk, ...args);
52
- };
53
- process.stdout.write = filter(state.stdout);
54
- process.stderr.write = filter(state.stderr);
55
- }
56
- state.depth += 1;
57
- try {
58
- return await operation();
59
- }
60
- finally {
61
- state.depth -= 1;
62
- if (state.depth === 0 && state.stdout && state.stderr) {
63
- process.stdout.write = state.stdout;
64
- process.stderr.write = state.stderr;
65
- state.stdout = undefined;
66
- state.stderr = undefined;
63
+ const filtered = filterNativeOcrNoiseText(text);
64
+ if (!filtered) {
65
+ const callback = args.find((arg) => typeof arg === 'function');
66
+ if (callback)
67
+ process.nextTick(callback);
68
+ return true;
67
69
  }
68
- }
69
- }
70
- // 临时屏蔽 Tesseract 底层 WASM 污染日志
71
- function withSilencedConsole(fn) {
72
- const originalLog = console.log;
73
- const originalWarn = console.warn;
74
- const originalError = console.error;
75
- const filter = (orig) => (...args) => {
76
- const msg = String(args[0] || '');
77
- if (msg.includes('Line cannot be recognized') || msg.includes('Image too small to scale')) {
70
+ const nextChunk = typeof chunk === 'string' ? filtered : Buffer.from(filtered, 'utf8');
71
+ return original.call(this, nextChunk, ...args);
72
+ };
73
+ const filterConsole = (original) => (...args) => {
74
+ if (args.length === 1 && typeof args[0] === 'string' && isOcrNoiseLine(args[0]))
78
75
  return;
79
- }
80
- orig(...args);
76
+ original(...args);
81
77
  };
82
- console.log = filter(originalLog);
83
- console.warn = filter(originalWarn);
84
- console.error = filter(originalError);
85
- try {
86
- const result = fn();
87
- if (result instanceof Promise) {
88
- return result.finally(() => {
89
- console.log = originalLog;
90
- console.warn = originalWarn;
91
- console.error = originalError;
92
- });
93
- }
94
- console.log = originalLog;
95
- console.warn = originalWarn;
96
- console.error = originalError;
97
- return result;
98
- }
99
- catch (error) {
100
- console.log = originalLog;
101
- console.warn = originalWarn;
102
- console.error = originalError;
103
- throw error;
104
- }
78
+ process.stdout.write = filterWrite(state.stdout);
79
+ process.stderr.write = filterWrite(state.stderr);
80
+ console.log = filterConsole(state.log);
81
+ console.warn = filterConsole(state.warn);
82
+ console.error = filterConsole(state.error);
105
83
  }
106
84
  export class TesseractJsProvider {
107
85
  id = 'tesseract.js';
@@ -154,7 +132,7 @@ export class TesseractJsProvider {
154
132
  return { text: '', confidence: 0, regions: [], warnings: [`image too small for OCR: ${width}x${height}`] };
155
133
  }
156
134
  try {
157
- const worker = await withSilencedConsole(() => this.getWorker());
135
+ ensureOcrNoiseSuppressed();
158
136
  let unlock;
159
137
  const nextLock = new Promise(resolve => { unlock = resolve; });
160
138
  const currentLock = this.workerLock;
@@ -162,7 +140,8 @@ export class TesseractJsProvider {
162
140
  await currentLock;
163
141
  let result;
164
142
  try {
165
- result = await withSilencedConsole(() => suppressNativeOcrNoise(() => worker.recognize(pngPath)));
143
+ const worker = await this.getWorker();
144
+ result = await this.recognizeWithTimeout(worker, pngPath);
166
145
  }
167
146
  finally {
168
147
  unlock();
@@ -191,6 +170,41 @@ export class TesseractJsProvider {
191
170
  getWarnings() {
192
171
  return [...new Set(this.warnings)].slice(-20);
193
172
  }
173
+ pushWarning(message) {
174
+ const text = message.trim();
175
+ if (!text || isOcrNoiseLine(text))
176
+ return;
177
+ this.warnings.push(text);
178
+ if (this.warnings.length > 50)
179
+ this.warnings = this.warnings.slice(-50);
180
+ }
181
+ async recognizeWithTimeout(worker, imagePath) {
182
+ const timeoutMs = 120_000;
183
+ let timeout;
184
+ try {
185
+ return await Promise.race([
186
+ worker.recognize(imagePath),
187
+ new Promise((_, reject) => {
188
+ timeout = setTimeout(() => reject(new Error(`OCR recognition timed out after ${timeoutMs}ms`)), timeoutMs);
189
+ }),
190
+ ]);
191
+ }
192
+ catch (error) {
193
+ await this.resetWorker().catch(() => undefined);
194
+ throw error;
195
+ }
196
+ finally {
197
+ if (timeout)
198
+ clearTimeout(timeout);
199
+ }
200
+ }
201
+ async resetWorker() {
202
+ const worker = this.worker;
203
+ this.worker = null;
204
+ this.workerPromise = null;
205
+ if (worker)
206
+ await worker.terminate();
207
+ }
194
208
  async readImageDimensions(filePath) {
195
209
  try {
196
210
  const sharpMod = await resolveAndImport('sharp');
@@ -210,8 +224,12 @@ export class TesseractJsProvider {
210
224
  async getWorker() {
211
225
  if (this.worker)
212
226
  return this.worker;
213
- if (!this.workerPromise)
214
- this.workerPromise = this.createReusableWorker();
227
+ if (!this.workerPromise) {
228
+ this.workerPromise = this.createReusableWorker().catch(error => {
229
+ this.workerPromise = null;
230
+ throw error;
231
+ });
232
+ }
215
233
  this.worker = await this.workerPromise;
216
234
  return this.worker;
217
235
  }
@@ -225,9 +243,16 @@ export class TesseractJsProvider {
225
243
  gzip: false,
226
244
  logger: () => undefined,
227
245
  errorHandler: (error) => {
228
- const message = error instanceof Error ? error.message : String(error);
229
- if (message.trim())
230
- this.warnings.push(message.trim());
246
+ let message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';
247
+ if (!message) {
248
+ try {
249
+ message = JSON.stringify(error);
250
+ }
251
+ catch {
252
+ message = String(error);
253
+ }
254
+ }
255
+ this.pushWarning(message);
231
256
  },
232
257
  });
233
258
  if (typeof worker.setParameters === 'function') {
@@ -236,10 +261,17 @@ export class TesseractJsProvider {
236
261
  return worker;
237
262
  }
238
263
  async dispose() {
239
- if (this.worker)
240
- await this.worker.terminate();
241
- this.worker = null;
242
- this.workerPromise = null;
264
+ let unlock;
265
+ const nextLock = new Promise(resolve => { unlock = resolve; });
266
+ const currentLock = this.workerLock;
267
+ this.workerLock = currentLock.then(() => nextLock).catch(() => nextLock);
268
+ await currentLock;
269
+ try {
270
+ await this.resetWorker();
271
+ }
272
+ finally {
273
+ unlock();
274
+ }
243
275
  }
244
276
  }
245
277
  // ─── 工厂 ───────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "4.0.26",
3
+ "version": "4.0.28",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",