id-scanner-lib 1.6.7 → 1.7.0

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.
@@ -62,19 +62,22 @@ export class ConsoleLogHandler implements LogHandler {
62
62
  handle(entry: LogEntry): void {
63
63
  const timestamp = new Date(entry.timestamp).toISOString();
64
64
  const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
65
-
65
+
66
+ // 优先使用 error,其次使用 data
67
+ const extra = entry.error || entry.data || '';
68
+
66
69
  switch (entry.level) {
67
70
  case LoggerLevel.DEBUG:
68
- console.debug(prefix, entry.message, entry.error || '');
71
+ console.debug(prefix, entry.message, extra);
69
72
  break;
70
73
  case LoggerLevel.INFO:
71
- console.info(prefix, entry.message, entry.error || '');
74
+ console.info(prefix, entry.message, extra);
72
75
  break;
73
76
  case LoggerLevel.WARN:
74
- console.warn(prefix, entry.message, entry.error || '');
77
+ console.warn(prefix, entry.message, extra);
75
78
  break;
76
79
  case LoggerLevel.ERROR:
77
- console.error(prefix, entry.message, entry.error || '');
80
+ console.error(prefix, entry.message, extra);
78
81
  break;
79
82
  default:
80
83
  // 输出什么也不做
@@ -158,10 +161,14 @@ export class RemoteLogHandler implements LogHandler {
158
161
  /** 发送间隔(毫秒) */
159
162
  private flushInterval: number;
160
163
  /** 定时发送的计时器ID */
161
- private timerId: number | null = null;
164
+ private timerId: ReturnType<typeof setInterval> | null = null;
162
165
  /** 是否在浏览器环境 */
163
166
  private readonly isBrowser: boolean;
164
-
167
+ /** 最大连续失败次数,超过则丢弃日志 */
168
+ private readonly maxConsecutiveFailures: number;
169
+ /** 当前连续失败计数 */
170
+ private consecutiveFailures: number = 0;
171
+
165
172
  /**
166
173
  * 构造函数
167
174
  * @param endpoint 远程服务器URL
@@ -173,10 +180,11 @@ export class RemoteLogHandler implements LogHandler {
173
180
  this.maxQueueSize = maxQueueSize;
174
181
  this.flushInterval = flushInterval;
175
182
  this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
176
-
183
+ this.maxConsecutiveFailures = 10;
184
+
177
185
  // 设置定时发送
178
186
  this.startTimer();
179
-
187
+
180
188
  // 页面卸载前尝试发送剩余日志
181
189
  if (this.isBrowser) {
182
190
  window.addEventListener('beforeunload', () => {
@@ -184,7 +192,7 @@ export class RemoteLogHandler implements LogHandler {
184
192
  });
185
193
  }
186
194
  }
187
-
195
+
188
196
  /**
189
197
  * 处理日志条目
190
198
  * @param entry 日志条目
@@ -193,86 +201,96 @@ export class RemoteLogHandler implements LogHandler {
193
201
  // 只处理INFO以上级别的日志
194
202
  if (entry.level >= LoggerLevel.INFO) {
195
203
  this.queue.push(entry);
196
-
204
+
197
205
  // 如果队列满了,立即发送
198
206
  if (this.queue.length >= this.maxQueueSize) {
199
207
  this.flush();
200
208
  }
201
209
  }
202
210
  }
203
-
211
+
204
212
  /**
205
213
  * 发送队列中的日志
206
214
  */
207
215
  flush(): void {
208
216
  if (this.queue.length === 0) return;
209
-
210
- const entriesToSend = [...this.queue];
211
- this.queue = [];
212
-
213
- // 防止在 fetch 失败时无限重试
214
- const sendCount = (this as any)._sendCount || 0;
215
- (this as any)._sendCount = sendCount + 1;
216
-
217
- // 如果发送次数过多,停止发送以防止无限循环
218
- if (sendCount > 10) {
219
- console.warn('RemoteLogHandler: Too many failed sends, stopping. Clear queue.');
217
+
218
+ // 如果连续失败次数过多,停止发送以防止无限循环
219
+ if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
220
+ console.warn('RemoteLogHandler: Too many consecutive failures, stopping. Clear queue.');
220
221
  this.queue = [];
221
- (this as any)._sendCount = 0;
222
+ this.consecutiveFailures = 0;
222
223
  return;
223
224
  }
224
-
225
- try {
226
- fetch(this.endpoint, {
227
- method: 'POST',
228
- headers: {
229
- 'Content-Type': 'application/json'
230
- },
231
- body: JSON.stringify(entriesToSend),
232
- keepalive: true
233
- }).catch((err: Error) => {
234
- console.error('Failed to send logs to remote server:', err);
235
-
236
- // 防止无限重试 - 如果失败次数过多,丢弃日志
237
- if ((this as any)._sendCount > 10) {
238
- console.warn('RemoteLogHandler: Max retry exceeded, discarding logs');
239
- this.queue = []; // 清空队列,避免内存泄漏
240
- (this as any)._sendCount = 0;
241
- return;
242
- }
243
-
244
- // 失败时把日志放回队列,但防止无限增长
245
- if (this.queue.length < this.maxQueueSize) {
246
- const maxReturn = Math.min(entriesToSend.length, this.maxQueueSize - this.queue.length);
247
- const returnedEntries = entriesToSend.slice(0, maxReturn);
248
- this.queue = [...returnedEntries, ...this.queue];
249
- }
250
-
251
- (this as any)._sendCount = 0;
252
- });
253
- } catch (error) {
254
- console.error('Error sending logs:', error);
255
- }
225
+
226
+ const entriesToSend = [...this.queue];
227
+ this.queue = [];
228
+
229
+ this.sendLogEntries(entriesToSend);
256
230
  }
257
-
231
+
232
+ /**
233
+ * 发送日志条目到远程服务器
234
+ * @param entries 日志条目数组
235
+ */
236
+ private sendLogEntries(entries: LogEntry[]): void {
237
+ if (entries.length === 0) return;
238
+
239
+ const controller = new AbortController();
240
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s 超时
241
+
242
+ fetch(this.endpoint, {
243
+ method: 'POST',
244
+ headers: {
245
+ 'Content-Type': 'application/json'
246
+ },
247
+ body: JSON.stringify(entries),
248
+ keepalive: true,
249
+ signal: controller.signal
250
+ }).then(() => {
251
+ clearTimeout(timeoutId);
252
+ this.consecutiveFailures = 0; // 发送成功,重置失败计数
253
+ }).catch((err: Error) => {
254
+ clearTimeout(timeoutId);
255
+ console.error('Failed to send logs to remote server:', err);
256
+
257
+ this.consecutiveFailures++;
258
+
259
+ // 如果失败次数过多,丢弃日志防止内存泄漏
260
+ if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
261
+ console.warn('RemoteLogHandler: Max consecutive failures exceeded, discarding logs');
262
+ this.queue = [];
263
+ this.consecutiveFailures = 0;
264
+ return;
265
+ }
266
+
267
+ // 失败时把日志放回队列,但防止无限增长
268
+ const maxReturn = Math.min(entries.length, this.maxQueueSize - this.queue.length);
269
+ if (maxReturn > 0) {
270
+ const returnedEntries = entries.slice(0, maxReturn);
271
+ this.queue = [...returnedEntries, ...this.queue];
272
+ }
273
+ });
274
+ }
275
+
258
276
  /**
259
277
  * 开始定时发送
260
278
  */
261
279
  startTimer(): void {
262
280
  if (!this.isBrowser) return;
263
281
  if (this.timerId !== null) return;
264
-
265
- this.timerId = window.setInterval(() => {
282
+
283
+ this.timerId = setInterval(() => {
266
284
  this.flush();
267
285
  }, this.flushInterval);
268
286
  }
269
-
287
+
270
288
  /**
271
289
  * 停止定时发送
272
290
  */
273
291
  stopTimer(): void {
274
292
  if (this.timerId !== null) {
275
- window.clearInterval(this.timerId);
293
+ clearInterval(this.timerId);
276
294
  this.timerId = null;
277
295
  }
278
296
  }
@@ -364,40 +382,56 @@ export class Logger {
364
382
  * 记录调试级别日志
365
383
  * @param tag 标签
366
384
  * @param message 消息
367
- * @param error 错误
385
+ * @param errorOrData 错误对象或结构化数据
368
386
  */
369
- debug(tag: string, message: string, error?: Error): void {
370
- this.log(LoggerLevel.DEBUG, tag, message, error);
387
+ debug(tag: string, message: string, errorOrData?: Error | object): void {
388
+ if (errorOrData instanceof Error) {
389
+ this.log(LoggerLevel.DEBUG, tag, message, errorOrData);
390
+ } else {
391
+ this.logWithData(LoggerLevel.DEBUG, tag, message, errorOrData);
392
+ }
371
393
  }
372
-
394
+
373
395
  /**
374
396
  * 记录信息级别日志
375
397
  * @param tag 标签
376
398
  * @param message 消息
377
- * @param error 错误
399
+ * @param errorOrData 错误对象或结构化数据
378
400
  */
379
- info(tag: string, message: string, error?: Error): void {
380
- this.log(LoggerLevel.INFO, tag, message, error);
401
+ info(tag: string, message: string, errorOrData?: Error | object): void {
402
+ if (errorOrData instanceof Error) {
403
+ this.log(LoggerLevel.INFO, tag, message, errorOrData);
404
+ } else {
405
+ this.logWithData(LoggerLevel.INFO, tag, message, errorOrData);
406
+ }
381
407
  }
382
-
408
+
383
409
  /**
384
410
  * 记录警告级别日志
385
411
  * @param tag 标签
386
412
  * @param message 消息
387
- * @param error 错误
413
+ * @param errorOrData 错误对象或结构化数据
388
414
  */
389
- warn(tag: string, message: string, error?: Error): void {
390
- this.log(LoggerLevel.WARN, tag, message, error);
415
+ warn(tag: string, message: string, errorOrData?: Error | object): void {
416
+ if (errorOrData instanceof Error) {
417
+ this.log(LoggerLevel.WARN, tag, message, errorOrData);
418
+ } else {
419
+ this.logWithData(LoggerLevel.WARN, tag, message, errorOrData);
420
+ }
391
421
  }
392
-
422
+
393
423
  /**
394
424
  * 记录错误级别日志
395
425
  * @param tag 标签
396
426
  * @param message 消息
397
- * @param error 错误
427
+ * @param errorOrData 错误对象或结构化数据
398
428
  */
399
- error(tag: string, message: string, error?: Error): void {
400
- this.log(LoggerLevel.ERROR, tag, message, error);
429
+ error(tag: string, message: string, errorOrData?: Error | object): void {
430
+ if (errorOrData instanceof Error) {
431
+ this.log(LoggerLevel.ERROR, tag, message, errorOrData);
432
+ } else {
433
+ this.logWithData(LoggerLevel.ERROR, tag, message, errorOrData);
434
+ }
401
435
  }
402
436
 
403
437
  /**
@@ -448,6 +482,46 @@ export class Logger {
448
482
  }
449
483
  }
450
484
 
485
+ /**
486
+ * 记录日志(支持结构化数据)
487
+ * @param level 日志级别
488
+ * @param tag 标签
489
+ * @param message 消息
490
+ * @param data 结构化数据
491
+ */
492
+ private logWithData(level: LoggerLevel, tag: string, message: string, data?: object): void {
493
+ // 检查日志级别
494
+ const levelValue = this.getLevelValue(level);
495
+ const currentLevelValue = this.getLevelValue(this.logLevel);
496
+
497
+ if (levelValue < currentLevelValue) {
498
+ return;
499
+ }
500
+
501
+ // 创建日志条目
502
+ const entry: LogEntry = {
503
+ timestamp: Date.now(),
504
+ level: level,
505
+ tag: tag || this.defaultTag,
506
+ message,
507
+ data
508
+ };
509
+
510
+ // 分发到所有处理程序
511
+ for (const handler of this.handlers) {
512
+ try {
513
+ handler.handle(entry);
514
+ } catch (handlerError) {
515
+ console.error(`[Logger] 处理程序错误:`, handlerError);
516
+ }
517
+ }
518
+
519
+ // 如果没有处理程序,使用控制台
520
+ if (this.handlers.length === 0) {
521
+ this.consoleOutput(entry);
522
+ }
523
+ }
524
+
451
525
  /**
452
526
  * 控制台输出
453
527
  * @param entry 日志条目
@@ -455,19 +529,22 @@ export class Logger {
455
529
  private consoleOutput(entry: LogEntry): void {
456
530
  const timestamp = new Date(entry.timestamp).toISOString();
457
531
  const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
458
-
532
+
533
+ // 构造日志内容:错误对象或数据对象
534
+ const extra = entry.error || entry.data || '';
535
+
459
536
  switch (entry.level) {
460
537
  case LoggerLevel.DEBUG:
461
- console.debug(`${prefix} ${entry.message}`, entry.error || '');
538
+ console.debug(`${prefix} ${entry.message}`, extra);
462
539
  break;
463
540
  case LoggerLevel.INFO:
464
- console.info(`${prefix} ${entry.message}`, entry.error || '');
541
+ console.info(`${prefix} ${entry.message}`, extra);
465
542
  break;
466
543
  case LoggerLevel.WARN:
467
- console.warn(`${prefix} ${entry.message}`, entry.error || '');
544
+ console.warn(`${prefix} ${entry.message}`, extra);
468
545
  break;
469
546
  case LoggerLevel.ERROR:
470
- console.error(`${prefix} ${entry.message}`, entry.error || '');
547
+ console.error(`${prefix} ${entry.message}`, extra);
471
548
  break;
472
549
  }
473
550
  }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @file 人脸比对器
3
+ * @description 提供人脸特征向量比对功能
4
+ * @module modules/face/face-comparator
5
+ */
6
+
7
+ import { FaceDetectionResult } from '../../interfaces/face-detection';
8
+ import { Result } from '../../core/result';
9
+ import { FaceComparisonError } from '../../core/errors';
10
+ import { Logger } from '../../core/logger';
11
+
12
+ /**
13
+ * 人脸比对结果
14
+ */
15
+ export interface ComparisonResult {
16
+ /** 相似度 (0-1) */
17
+ similarity: number;
18
+ /** 是否匹配(基于阈值) */
19
+ isMatch: boolean;
20
+ /** 使用的阈值 */
21
+ threshold: number;
22
+ }
23
+
24
+ /**
25
+ * 人脸比对器配置
26
+ */
27
+ export interface FaceComparatorConfig {
28
+ /** 人脸匹配阈值 (0-1) */
29
+ matchThreshold?: number;
30
+ }
31
+
32
+ /**
33
+ * 人脸比对器
34
+ *
35
+ * 负责计算两个人脸特征向量的相似度
36
+ */
37
+ export class FaceComparator {
38
+ /** 日志记录器 */
39
+ private logger: Logger;
40
+
41
+ /** 匹配阈值 */
42
+ private matchThreshold: number;
43
+
44
+ /**
45
+ * 构造函数
46
+ * @param config 比对器配置
47
+ */
48
+ constructor(config: FaceComparatorConfig = {}) {
49
+ this.logger = Logger.getInstance();
50
+ this.matchThreshold = config.matchThreshold ?? 0.6;
51
+ }
52
+
53
+ /**
54
+ * 计算两个特征向量的余弦相似度
55
+ *
56
+ * @param v1 特征向量1
57
+ * @param v2 特征向量2
58
+ * @returns 相似度 (0-1)
59
+ */
60
+ computeSimilarity(v1: number[], v2: number[]): number {
61
+ if (v1.length !== v2.length) {
62
+ throw new Error('特征向量维度不匹配');
63
+ }
64
+
65
+ let dotProduct = 0;
66
+ let norm1 = 0;
67
+ let norm2 = 0;
68
+
69
+ for (let i = 0; i < v1.length; i++) {
70
+ dotProduct += v1[i] * v2[i];
71
+ norm1 += v1[i] * v1[i];
72
+ norm2 += v2[i] * v2[i];
73
+ }
74
+
75
+ // 确保长度非零
76
+ if (norm1 === 0 || norm2 === 0) {
77
+ return 0;
78
+ }
79
+
80
+ return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
81
+ }
82
+
83
+ /**
84
+ * 比对两个人脸
85
+ *
86
+ * @param source 源人脸(特征向量或检测结果)
87
+ * @param target 目标人脸(特征向量或检测结果)
88
+ * @returns 比对结果
89
+ */
90
+ compare(
91
+ source: number[] | FaceDetectionResult,
92
+ target: number[] | FaceDetectionResult
93
+ ): Result<ComparisonResult> {
94
+ try {
95
+ // 提取特征向量
96
+ const sourceEmbedding = this.extractEmbedding(source);
97
+ const targetEmbedding = this.extractEmbedding(target);
98
+
99
+ if (!sourceEmbedding || !targetEmbedding) {
100
+ return Result.failure(new FaceComparisonError('无法获取特征向量'));
101
+ }
102
+
103
+ // 计算相似度
104
+ const similarity = this.computeSimilarity(sourceEmbedding, targetEmbedding);
105
+ const isMatch = similarity >= this.matchThreshold;
106
+
107
+ return Result.success({
108
+ similarity,
109
+ isMatch,
110
+ threshold: this.matchThreshold
111
+ });
112
+ } catch (error) {
113
+ const errorMessage = error instanceof Error ? error.message : String(error);
114
+ this.logger.error('FaceComparator', `人脸比对失败: ${errorMessage}`, error as Error);
115
+ return Result.failure(new FaceComparisonError(`人脸比对失败: ${errorMessage}`));
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 从输入中提取特征向量
121
+ */
122
+ private extractEmbedding(input: number[] | FaceDetectionResult): number[] | null {
123
+ if (Array.isArray(input)) {
124
+ return input;
125
+ }
126
+
127
+ if (input.embedding?.vector) {
128
+ return input.embedding.vector;
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * 设置匹配阈值
136
+ */
137
+ setThreshold(threshold: number): void {
138
+ if (threshold < 0 || threshold > 1) {
139
+ throw new Error('阈值必须在 0-1 范围内');
140
+ }
141
+ this.matchThreshold = threshold;
142
+ }
143
+
144
+ /**
145
+ * 获取当前阈值
146
+ */
147
+ getThreshold(): number {
148
+ return this.matchThreshold;
149
+ }
150
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @file 人脸检测选项工厂
3
+ * @description 创建 face-api 检测选项和处理选项
4
+ * @module modules/face/face-detector-options
5
+ */
6
+
7
+ import * as faceapi from '@vladmandic/face-api';
8
+ import { FaceDetectionOptions } from '../../interfaces/face-detection';
9
+ import { FaceModelType, FaceDetectorConfig } from './face-detector';
10
+
11
+ /**
12
+ * 人脸检测选项工厂
13
+ *
14
+ * 负责创建 face-api 兼容的检测选项和处理选项
15
+ */
16
+ export class FaceDetectorOptionsFactory {
17
+ /**
18
+ * 创建 face-api 检测选项
19
+ * @param detectionModel 检测模型类型
20
+ * @param minConfidence 最小置信度
21
+ * @returns face-api 检测选项
22
+ */
23
+ static createFaceAPIOptions(
24
+ detectionModel: FaceModelType,
25
+ minConfidence: number
26
+ ): faceapi.SsdMobilenetv1Options | faceapi.TinyFaceDetectorOptions | faceapi.MtcnnOptions {
27
+ switch (detectionModel) {
28
+ case FaceModelType.SSD_MOBILENET:
29
+ return new faceapi.SsdMobilenetv1Options({ minConfidence });
30
+ case FaceModelType.TINY_FACE:
31
+ return new faceapi.TinyFaceDetectorOptions({ scoreThreshold: minConfidence });
32
+ case FaceModelType.MTCNN:
33
+ return new faceapi.MtcnnOptions({ minConfidence });
34
+ default:
35
+ return new faceapi.SsdMobilenetv1Options({ minConfidence });
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 合并检测选项和配置
41
+ * @param config 人脸检测器配置
42
+ * @param options 用户提供的选项
43
+ * @returns 合并后的检测选项
44
+ */
45
+ static mergeOptions(
46
+ config: FaceDetectorConfig,
47
+ options: FaceDetectionOptions = {}
48
+ ): FaceDetectionOptions {
49
+ return {
50
+ minConfidence: config.minConfidence,
51
+ maxFaces: config.maxFaces,
52
+ withLandmarks: config.detectLandmarks,
53
+ withAttributes: config.detectExpressions || config.detectAgeGender,
54
+ withEmbedding: config.extractEmbeddings,
55
+ enableTracking: config.enableTracking,
56
+ ...options
57
+ };
58
+ }
59
+
60
+ /**
61
+ * 执行人脸检测
62
+ * @param input 输入图像/视频/画布
63
+ * @param faceapiOptions face-api 检测选项
64
+ * @param detectOptions 检测选项
65
+ * @param landmarksModel 关键点模型类型
66
+ * @returns face-api 检测结果
67
+ */
68
+ static async detect(
69
+ input: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement,
70
+ faceapiOptions: faceapi.SsdMobilenetv1Options | faceapi.TinyFaceDetectorOptions | faceapi.MtcnnOptions,
71
+ detectOptions: FaceDetectionOptions,
72
+ landmarksModel: 'tiny' | '68_points'
73
+ ): Promise<any> {
74
+ const withLandmarks = detectOptions.withLandmarks;
75
+ const withAttributes = detectOptions.withAttributes;
76
+ const withEmbedding = detectOptions.withEmbedding;
77
+
78
+ // 根据选项组合选择检测方法链
79
+ if (withLandmarks && withAttributes && withEmbedding) {
80
+ // 全功能检测
81
+ return await faceapi
82
+ .detectAllFaces(input, faceapiOptions)
83
+ .withFaceLandmarks(landmarksModel === 'tiny')
84
+ .withFaceExpressions()
85
+ .withAgeAndGender()
86
+ .withFaceDescriptors();
87
+ } else if (withLandmarks && withAttributes) {
88
+ // 检测带关键点和属性
89
+ return await faceapi
90
+ .detectAllFaces(input, faceapiOptions)
91
+ .withFaceLandmarks(landmarksModel === 'tiny')
92
+ .withFaceExpressions()
93
+ .withAgeAndGender();
94
+ } else if (withLandmarks) {
95
+ // 检测带关键点
96
+ return await faceapi
97
+ .detectAllFaces(input, faceapiOptions)
98
+ .withFaceLandmarks(landmarksModel === 'tiny');
99
+ } else {
100
+ // 仅检测
101
+ return await faceapi.detectAllFaces(input, faceapiOptions);
102
+ }
103
+ }
104
+ }