This is page 7 of 10. Use http://codebase.md/hangwin/mcp-chrome?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ └── workflows
│ └── build-release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│ └── extensions.json
├── app
│ ├── chrome-extension
│ │ ├── _locales
│ │ │ ├── de
│ │ │ │ └── messages.json
│ │ │ ├── en
│ │ │ │ └── messages.json
│ │ │ ├── ja
│ │ │ │ └── messages.json
│ │ │ ├── ko
│ │ │ │ └── messages.json
│ │ │ ├── zh_CN
│ │ │ │ └── messages.json
│ │ │ └── zh_TW
│ │ │ └── messages.json
│ │ ├── .env.example
│ │ ├── assets
│ │ │ └── vue.svg
│ │ ├── common
│ │ │ ├── constants.ts
│ │ │ ├── message-types.ts
│ │ │ └── tool-handler.ts
│ │ ├── entrypoints
│ │ │ ├── background
│ │ │ │ ├── index.ts
│ │ │ │ ├── native-host.ts
│ │ │ │ ├── semantic-similarity.ts
│ │ │ │ ├── storage-manager.ts
│ │ │ │ └── tools
│ │ │ │ ├── base-browser.ts
│ │ │ │ ├── browser
│ │ │ │ │ ├── bookmark.ts
│ │ │ │ │ ├── common.ts
│ │ │ │ │ ├── console.ts
│ │ │ │ │ ├── file-upload.ts
│ │ │ │ │ ├── history.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-script.ts
│ │ │ │ │ ├── interaction.ts
│ │ │ │ │ ├── keyboard.ts
│ │ │ │ │ ├── network-capture-debugger.ts
│ │ │ │ │ ├── network-capture-web-request.ts
│ │ │ │ │ ├── network-request.ts
│ │ │ │ │ ├── screenshot.ts
│ │ │ │ │ ├── vector-search.ts
│ │ │ │ │ ├── web-fetcher.ts
│ │ │ │ │ └── window.ts
│ │ │ │ └── index.ts
│ │ │ ├── content.ts
│ │ │ ├── offscreen
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ │ └── popup
│ │ │ ├── App.vue
│ │ │ ├── components
│ │ │ │ ├── ConfirmDialog.vue
│ │ │ │ ├── icons
│ │ │ │ │ ├── BoltIcon.vue
│ │ │ │ │ ├── CheckIcon.vue
│ │ │ │ │ ├── DatabaseIcon.vue
│ │ │ │ │ ├── DocumentIcon.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── TabIcon.vue
│ │ │ │ │ ├── TrashIcon.vue
│ │ │ │ │ └── VectorIcon.vue
│ │ │ │ ├── ModelCacheManagement.vue
│ │ │ │ └── ProgressIndicator.vue
│ │ │ ├── index.html
│ │ │ ├── main.ts
│ │ │ └── style.css
│ │ ├── eslint.config.js
│ │ ├── inject-scripts
│ │ │ ├── click-helper.js
│ │ │ ├── fill-helper.js
│ │ │ ├── inject-bridge.js
│ │ │ ├── interactive-elements-helper.js
│ │ │ ├── keyboard-helper.js
│ │ │ ├── network-helper.js
│ │ │ ├── screenshot-helper.js
│ │ │ └── web-fetcher-helper.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── icon
│ │ │ │ ├── 128.png
│ │ │ │ ├── 16.png
│ │ │ │ ├── 32.png
│ │ │ │ ├── 48.png
│ │ │ │ └── 96.png
│ │ │ ├── libs
│ │ │ │ └── ort.min.js
│ │ │ └── wxt.svg
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ ├── utils
│ │ │ ├── content-indexer.ts
│ │ │ ├── i18n.ts
│ │ │ ├── image-utils.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── model-cache-manager.ts
│ │ │ ├── offscreen-manager.ts
│ │ │ ├── semantic-similarity-engine.ts
│ │ │ ├── simd-math-engine.ts
│ │ │ ├── text-chunker.ts
│ │ │ └── vector-database.ts
│ │ ├── workers
│ │ │ ├── ort-wasm-simd-threaded.jsep.mjs
│ │ │ ├── ort-wasm-simd-threaded.jsep.wasm
│ │ │ ├── ort-wasm-simd-threaded.mjs
│ │ │ ├── ort-wasm-simd-threaded.wasm
│ │ │ ├── simd_math_bg.wasm
│ │ │ ├── simd_math.js
│ │ │ └── similarity.worker.js
│ │ └── wxt.config.ts
│ └── native-server
│ ├── debug.sh
│ ├── install.md
│ ├── jest.config.js
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── cli.ts
│ │ ├── constant
│ │ │ └── index.ts
│ │ ├── file-handler.ts
│ │ ├── index.ts
│ │ ├── mcp
│ │ │ ├── mcp-server-stdio.ts
│ │ │ ├── mcp-server.ts
│ │ │ ├── register-tools.ts
│ │ │ └── stdio-config.json
│ │ ├── native-messaging-host.ts
│ │ ├── scripts
│ │ │ ├── browser-config.ts
│ │ │ ├── build.ts
│ │ │ ├── constant.ts
│ │ │ ├── postinstall.ts
│ │ │ ├── register-dev.ts
│ │ │ ├── register.ts
│ │ │ ├── run_host.bat
│ │ │ ├── run_host.sh
│ │ │ └── utils.ts
│ │ ├── server
│ │ │ ├── index.ts
│ │ │ └── server.test.ts
│ │ └── util
│ │ └── logger.ts
│ └── tsconfig.json
├── commitlint.config.cjs
├── docs
│ ├── ARCHITECTURE_zh.md
│ ├── ARCHITECTURE.md
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING_zh.md
│ ├── CONTRIBUTING.md
│ ├── TOOLS_zh.md
│ ├── TOOLS.md
│ ├── TROUBLESHOOTING_zh.md
│ ├── TROUBLESHOOTING.md
│ └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│ ├── shared
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── tools.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ └── wasm-simd
│ ├── .gitignore
│ ├── BUILD.md
│ ├── Cargo.toml
│ ├── package.json
│ ├── README.md
│ └── src
│ └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│ ├── content-analize.md
│ ├── excalidraw-prompt.md
│ └── modify-web.md
├── README_zh.md
├── README.md
├── releases
│ ├── chrome-extension
│ │ └── latest
│ │ └── chrome-mcp-server-lastest.zip
│ └── README.md
└── test-inject-script.js
```
# Files
--------------------------------------------------------------------------------
/app/chrome-extension/utils/semantic-similarity-engine.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AutoTokenizer, env as TransformersEnv } from '@xenova/transformers';
2 | import type { Tensor as TransformersTensor, PreTrainedTokenizer } from '@xenova/transformers';
3 | import LRUCache from './lru-cache';
4 | import { SIMDMathEngine } from './simd-math-engine';
5 | import { OffscreenManager } from './offscreen-manager';
6 | import { STORAGE_KEYS } from '@/common/constants';
7 | import { OFFSCREEN_MESSAGE_TYPES } from '@/common/message-types';
8 |
9 | import { ModelCacheManager } from './model-cache-manager';
10 |
11 | /**
12 | * Get cached model data, prioritizing cache reads and handling redirected URLs.
13 | * @param {string} modelUrl Stable, permanent URL of the model
14 | * @returns {Promise<ArrayBuffer>} Model data as ArrayBuffer
15 | */
16 | async function getCachedModelData(modelUrl: string): Promise<ArrayBuffer> {
17 | const cacheManager = ModelCacheManager.getInstance();
18 |
19 | // 1. 尝试从缓存获取数据
20 | const cachedData = await cacheManager.getCachedModelData(modelUrl);
21 | if (cachedData) {
22 | return cachedData;
23 | }
24 |
25 | console.log('Model not found in cache or expired. Fetching from network...');
26 |
27 | try {
28 | // 2. 从网络获取数据
29 | const response = await fetch(modelUrl);
30 |
31 | if (!response.ok) {
32 | throw new Error(`Failed to fetch model: ${response.status} ${response.statusText}`);
33 | }
34 |
35 | // 3. 获取数据并存储到缓存
36 | const arrayBuffer = await response.arrayBuffer();
37 | await cacheManager.storeModelData(modelUrl, arrayBuffer);
38 |
39 | console.log(
40 | `Model fetched from network and successfully cached (${(arrayBuffer.byteLength / 1024 / 1024).toFixed(2)}MB).`,
41 | );
42 |
43 | return arrayBuffer;
44 | } catch (error) {
45 | console.error(`Error fetching or caching model:`, error);
46 | // 如果获取失败,清理可能不完整的缓存条目
47 | await cacheManager.deleteCacheEntry(modelUrl);
48 | throw error;
49 | }
50 | }
51 |
52 | /**
53 | * Clear all model cache entries
54 | */
55 | export async function clearModelCache(): Promise<void> {
56 | try {
57 | const cacheManager = ModelCacheManager.getInstance();
58 | await cacheManager.clearAllCache();
59 | } catch (error) {
60 | console.error('Failed to clear model cache:', error);
61 | throw error;
62 | }
63 | }
64 |
65 | /**
66 | * Get cache statistics
67 | */
68 | export async function getCacheStats(): Promise<{
69 | totalSize: number;
70 | totalSizeMB: number;
71 | entryCount: number;
72 | entries: Array<{
73 | url: string;
74 | size: number;
75 | sizeMB: number;
76 | timestamp: number;
77 | age: string;
78 | expired: boolean;
79 | }>;
80 | }> {
81 | try {
82 | const cacheManager = ModelCacheManager.getInstance();
83 | return await cacheManager.getCacheStats();
84 | } catch (error) {
85 | console.error('Failed to get cache stats:', error);
86 | throw error;
87 | }
88 | }
89 |
90 | /**
91 | * Manually trigger cache cleanup
92 | */
93 | export async function cleanupModelCache(): Promise<void> {
94 | try {
95 | const cacheManager = ModelCacheManager.getInstance();
96 | await cacheManager.manualCleanup();
97 | } catch (error) {
98 | console.error('Failed to cleanup cache:', error);
99 | throw error;
100 | }
101 | }
102 |
103 | /**
104 | * Check if the default model is cached and available
105 | * @returns Promise<boolean> True if default model is cached and valid
106 | */
107 | export async function isDefaultModelCached(): Promise<boolean> {
108 | try {
109 | // Get the default model configuration
110 | const result = await chrome.storage.local.get([STORAGE_KEYS.SEMANTIC_MODEL]);
111 | const defaultModel =
112 | (result[STORAGE_KEYS.SEMANTIC_MODEL] as ModelPreset) || 'multilingual-e5-small';
113 |
114 | // Build the model URL
115 | const modelInfo = PREDEFINED_MODELS[defaultModel];
116 | const modelIdentifier = modelInfo.modelIdentifier;
117 | const onnxModelFile = 'model.onnx'; // Default ONNX file name
118 |
119 | const modelIdParts = modelIdentifier.split('/');
120 | const modelNameForUrl = modelIdParts.length > 1 ? modelIdentifier : `Xenova/${modelIdentifier}`;
121 | const onnxModelUrl = `https://huggingface.co/${modelNameForUrl}/resolve/main/onnx/${onnxModelFile}`;
122 |
123 | // Check if this model is cached
124 | const cacheManager = ModelCacheManager.getInstance();
125 | return await cacheManager.isModelCached(onnxModelUrl);
126 | } catch (error) {
127 | console.error('Error checking if default model is cached:', error);
128 | return false;
129 | }
130 | }
131 |
132 | /**
133 | * Check if any model cache exists (for conditional initialization)
134 | * @returns Promise<boolean> True if any valid model cache exists
135 | */
136 | export async function hasAnyModelCache(): Promise<boolean> {
137 | try {
138 | const cacheManager = ModelCacheManager.getInstance();
139 | return await cacheManager.hasAnyValidCache();
140 | } catch (error) {
141 | console.error('Error checking for any model cache:', error);
142 | return false;
143 | }
144 | }
145 |
146 | // Predefined model configurations - 2025 curated recommended models, using quantized versions to reduce file size
147 | export const PREDEFINED_MODELS = {
148 | // Multilingual model - default recommendation
149 | 'multilingual-e5-small': {
150 | modelIdentifier: 'Xenova/multilingual-e5-small',
151 | dimension: 384,
152 | description: 'Multilingual E5 Small - Lightweight multilingual model supporting 100+ languages',
153 | language: 'multilingual',
154 | performance: 'excellent',
155 | size: '116MB', // Quantized version
156 | latency: '20ms',
157 | multilingualFeatures: {
158 | languageSupport: '100+',
159 | crossLanguageRetrieval: 'good',
160 | chineseEnglishMixed: 'good',
161 | },
162 | modelSpecificConfig: {
163 | requiresTokenTypeIds: false, // E5 model doesn't require token_type_ids
164 | },
165 | },
166 | 'multilingual-e5-base': {
167 | modelIdentifier: 'Xenova/multilingual-e5-base',
168 | dimension: 768,
169 | description: 'Multilingual E5 base - Medium-scale multilingual model supporting 100+ languages',
170 | language: 'multilingual',
171 | performance: 'excellent',
172 | size: '279MB', // Quantized version
173 | latency: '30ms',
174 | multilingualFeatures: {
175 | languageSupport: '100+',
176 | crossLanguageRetrieval: 'excellent',
177 | chineseEnglishMixed: 'excellent',
178 | },
179 | modelSpecificConfig: {
180 | requiresTokenTypeIds: false, // E5 model doesn't require token_type_ids
181 | },
182 | },
183 | } as const;
184 |
185 | export type ModelPreset = keyof typeof PREDEFINED_MODELS;
186 |
187 | /**
188 | * Get model information
189 | */
190 | export function getModelInfo(preset: ModelPreset) {
191 | return PREDEFINED_MODELS[preset];
192 | }
193 |
194 | /**
195 | * List all available models
196 | */
197 | export function listAvailableModels() {
198 | return Object.entries(PREDEFINED_MODELS).map(([key, value]) => ({
199 | preset: key as ModelPreset,
200 | ...value,
201 | }));
202 | }
203 |
204 | /**
205 | * Recommend model based on language - only uses multilingual-e5 series models
206 | */
207 | export function recommendModelForLanguage(
208 | _language: 'en' | 'zh' | 'multilingual' = 'multilingual',
209 | scenario: 'speed' | 'balanced' | 'quality' = 'balanced',
210 | ): ModelPreset {
211 | // All languages use multilingual models
212 | if (scenario === 'quality') {
213 | return 'multilingual-e5-base'; // High quality choice
214 | }
215 | return 'multilingual-e5-small'; // Default lightweight choice
216 | }
217 |
218 | /**
219 | * Intelligently recommend model based on device performance and usage scenario - only uses multilingual-e5 series models
220 | */
221 | export function recommendModelForDevice(
222 | _language: 'en' | 'zh' | 'multilingual' = 'multilingual',
223 | deviceMemory: number = 4, // GB
224 | networkSpeed: 'slow' | 'fast' = 'fast',
225 | prioritizeSpeed: boolean = false,
226 | ): ModelPreset {
227 | // Low memory devices or slow network, prioritize small models
228 | if (deviceMemory < 4 || networkSpeed === 'slow' || prioritizeSpeed) {
229 | return 'multilingual-e5-small'; // Lightweight choice
230 | }
231 |
232 | // High performance devices can use better models
233 | if (deviceMemory >= 8 && !prioritizeSpeed) {
234 | return 'multilingual-e5-base'; // High performance choice
235 | }
236 |
237 | // Default balanced choice
238 | return 'multilingual-e5-small';
239 | }
240 |
241 | /**
242 | * Get model size information (only supports quantized version)
243 | */
244 | export function getModelSizeInfo(
245 | preset: ModelPreset,
246 | _version: 'full' | 'quantized' | 'compressed' = 'quantized',
247 | ) {
248 | const model = PREDEFINED_MODELS[preset];
249 |
250 | return {
251 | size: model.size,
252 | recommended: 'quantized',
253 | description: `${model.description} (Size: ${model.size})`,
254 | };
255 | }
256 |
257 | /**
258 | * Compare performance and size of multiple models
259 | */
260 | export function compareModels(presets: ModelPreset[]) {
261 | return presets.map((preset) => {
262 | const model = PREDEFINED_MODELS[preset];
263 |
264 | return {
265 | preset,
266 | name: model.description.split(' - ')[0],
267 | language: model.language,
268 | performance: model.performance,
269 | dimension: model.dimension,
270 | latency: model.latency,
271 | size: model.size,
272 | features: (model as any).multilingualFeatures || {},
273 | maxLength: (model as any).maxLength || 512,
274 | recommendedFor: getRecommendationContext(preset),
275 | };
276 | });
277 | }
278 |
279 | /**
280 | * Get recommended use cases for model
281 | */
282 | function getRecommendationContext(preset: ModelPreset): string[] {
283 | const contexts: string[] = [];
284 | const model = PREDEFINED_MODELS[preset];
285 |
286 | // All models are multilingual
287 | contexts.push('Multilingual document processing');
288 |
289 | if (model.performance === 'excellent') contexts.push('High accuracy requirements');
290 | if (model.latency.includes('20ms')) contexts.push('Fast response');
291 |
292 | // Add scenarios based on model size
293 | const sizeInMB = parseInt(model.size.replace('MB', ''));
294 | if (sizeInMB < 300) {
295 | contexts.push('Mobile devices');
296 | contexts.push('Lightweight deployment');
297 | }
298 |
299 | if (preset === 'multilingual-e5-small') {
300 | contexts.push('Lightweight deployment');
301 | } else if (preset === 'multilingual-e5-base') {
302 | contexts.push('High accuracy requirements');
303 | }
304 |
305 | return contexts;
306 | }
307 |
308 | /**
309 | * Get ONNX model filename (only supports quantized version)
310 | */
311 | export function getOnnxFileNameForVersion(
312 | _version: 'full' | 'quantized' | 'compressed' = 'quantized',
313 | ): string {
314 | // Only return quantized version filename
315 | return 'model_quantized.onnx';
316 | }
317 |
318 | /**
319 | * Get model identifier (only supports quantized version)
320 | */
321 | export function getModelIdentifierWithVersion(
322 | preset: ModelPreset,
323 | _version: 'full' | 'quantized' | 'compressed' = 'quantized',
324 | ): string {
325 | const model = PREDEFINED_MODELS[preset];
326 | return model.modelIdentifier;
327 | }
328 |
329 | /**
330 | * Get size comparison of all available models
331 | */
332 | export function getAllModelSizes() {
333 | const models = Object.entries(PREDEFINED_MODELS).map(([preset, config]) => {
334 | return {
335 | preset: preset as ModelPreset,
336 | name: config.description.split(' - ')[0],
337 | language: config.language,
338 | size: config.size,
339 | performance: config.performance,
340 | latency: config.latency,
341 | };
342 | });
343 |
344 | // Sort by size
345 | return models.sort((a, b) => {
346 | const sizeA = parseInt(a.size.replace('MB', ''));
347 | const sizeB = parseInt(b.size.replace('MB', ''));
348 | return sizeA - sizeB;
349 | });
350 | }
351 |
352 | // Define necessary types
353 | interface ModelConfig {
354 | modelIdentifier: string;
355 | localModelPathPrefix?: string; // Base path for local models (relative to public)
356 | onnxModelFile?: string; // ONNX model filename
357 | maxLength?: number;
358 | cacheSize?: number;
359 | numThreads?: number;
360 | executionProviders?: string[];
361 | useLocalFiles?: boolean;
362 | workerPath?: string; // Worker script path (relative to extension root)
363 | concurrentLimit?: number; // Worker task concurrency limit
364 | forceOffscreen?: boolean; // Force offscreen mode (for testing)
365 | modelPreset?: ModelPreset; // Predefined model selection
366 | dimension?: number; // Vector dimension (auto-obtained from preset model)
367 | modelVersion?: 'full' | 'quantized' | 'compressed'; // Model version selection
368 | requiresTokenTypeIds?: boolean; // Whether model requires token_type_ids input
369 | }
370 |
371 | interface WorkerMessagePayload {
372 | modelPath?: string;
373 | modelData?: ArrayBuffer;
374 | numThreads?: number;
375 | executionProviders?: string[];
376 | input_ids?: number[];
377 | attention_mask?: number[];
378 | token_type_ids?: number[];
379 | dims?: {
380 | input_ids: number[];
381 | attention_mask: number[];
382 | token_type_ids?: number[];
383 | };
384 | }
385 |
386 | interface WorkerResponsePayload {
387 | data?: Float32Array | number[]; // Tensor data as Float32Array or number array
388 | dims?: number[]; // Tensor dimensions
389 | message?: string; // For error or status messages
390 | }
391 |
392 | interface WorkerStats {
393 | inferenceTime?: number;
394 | totalInferences?: number;
395 | averageInferenceTime?: number;
396 | memoryAllocations?: number;
397 | batchSize?: number;
398 | }
399 |
400 | // Memory pool manager
401 | class EmbeddingMemoryPool {
402 | private pools: Map<number, Float32Array[]> = new Map();
403 | private maxPoolSize: number = 10;
404 | private stats = { allocated: 0, reused: 0, released: 0 };
405 |
406 | getEmbedding(size: number): Float32Array {
407 | const pool = this.pools.get(size);
408 | if (pool && pool.length > 0) {
409 | this.stats.reused++;
410 | return pool.pop()!;
411 | }
412 |
413 | this.stats.allocated++;
414 | return new Float32Array(size);
415 | }
416 |
417 | releaseEmbedding(embedding: Float32Array): void {
418 | const size = embedding.length;
419 | if (!this.pools.has(size)) {
420 | this.pools.set(size, []);
421 | }
422 |
423 | const pool = this.pools.get(size)!;
424 | if (pool.length < this.maxPoolSize) {
425 | // Clear array for reuse
426 | embedding.fill(0);
427 | pool.push(embedding);
428 | this.stats.released++;
429 | }
430 | }
431 |
432 | getStats() {
433 | return { ...this.stats };
434 | }
435 |
436 | clear(): void {
437 | this.pools.clear();
438 | this.stats = { allocated: 0, reused: 0, released: 0 };
439 | }
440 | }
441 |
442 | interface PendingMessage {
443 | resolve: (value: WorkerResponsePayload | PromiseLike<WorkerResponsePayload>) => void;
444 | reject: (reason?: any) => void;
445 | type: string;
446 | }
447 |
448 | interface TokenizedOutput {
449 | // Simulates part of transformers.js tokenizer output
450 | input_ids: TransformersTensor;
451 | attention_mask: TransformersTensor;
452 | token_type_ids?: TransformersTensor;
453 | }
454 |
455 | /**
456 | * SemanticSimilarityEngine proxy class
457 | * Used by ContentIndexer and other components to reuse engine instance in offscreen, avoiding duplicate model downloads
458 | */
459 | export class SemanticSimilarityEngineProxy {
460 | private _isInitialized = false;
461 | private config: Partial<ModelConfig>;
462 | private offscreenManager: OffscreenManager;
463 | private _isEnsuring = false; // Flag to prevent concurrent ensureOffscreenEngineInitialized calls
464 |
465 | constructor(config: Partial<ModelConfig> = {}) {
466 | this.config = config;
467 | this.offscreenManager = OffscreenManager.getInstance();
468 | console.log('SemanticSimilarityEngineProxy: Proxy created with config:', {
469 | modelPreset: config.modelPreset,
470 | modelVersion: config.modelVersion,
471 | dimension: config.dimension,
472 | });
473 | }
474 |
475 | async initialize(): Promise<void> {
476 | try {
477 | console.log('SemanticSimilarityEngineProxy: Starting proxy initialization...');
478 |
479 | // Ensure offscreen document exists
480 | console.log('SemanticSimilarityEngineProxy: Ensuring offscreen document exists...');
481 | await this.offscreenManager.ensureOffscreenDocument();
482 | console.log('SemanticSimilarityEngineProxy: Offscreen document ready');
483 |
484 | // Ensure engine in offscreen is initialized
485 | console.log('SemanticSimilarityEngineProxy: Ensuring offscreen engine is initialized...');
486 | await this.ensureOffscreenEngineInitialized();
487 |
488 | this._isInitialized = true;
489 | console.log(
490 | 'SemanticSimilarityEngineProxy: Proxy initialized, delegating to offscreen engine',
491 | );
492 | } catch (error) {
493 | console.error('SemanticSimilarityEngineProxy: Initialization failed:', error);
494 | throw new Error(
495 | `Failed to initialize proxy: ${error instanceof Error ? error.message : 'Unknown error'}`,
496 | );
497 | }
498 | }
499 |
500 | /**
501 | * Check engine status in offscreen
502 | */
503 | private async checkOffscreenEngineStatus(): Promise<{
504 | isInitialized: boolean;
505 | currentConfig: any;
506 | }> {
507 | try {
508 | const response = await chrome.runtime.sendMessage({
509 | target: 'offscreen',
510 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_STATUS,
511 | });
512 |
513 | if (response && response.success) {
514 | return {
515 | isInitialized: response.isInitialized || false,
516 | currentConfig: response.currentConfig || null,
517 | };
518 | }
519 | } catch (error) {
520 | console.warn('SemanticSimilarityEngineProxy: Failed to check engine status:', error);
521 | }
522 |
523 | return { isInitialized: false, currentConfig: null };
524 | }
525 |
526 | /**
527 | * Ensure engine in offscreen is initialized (with concurrency protection)
528 | */
529 | private async ensureOffscreenEngineInitialized(): Promise<void> {
530 | // Prevent concurrent initialization attempts
531 | if (this._isEnsuring) {
532 | console.log('SemanticSimilarityEngineProxy: Already ensuring initialization, waiting...');
533 | // Wait a bit and check again
534 | await new Promise((resolve) => setTimeout(resolve, 100));
535 | return;
536 | }
537 |
538 | try {
539 | this._isEnsuring = true;
540 | const status = await this.checkOffscreenEngineStatus();
541 |
542 | if (!status.isInitialized) {
543 | console.log(
544 | 'SemanticSimilarityEngineProxy: Engine not initialized in offscreen, initializing...',
545 | );
546 |
547 | // Reinitialize engine
548 | const response = await chrome.runtime.sendMessage({
549 | target: 'offscreen',
550 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
551 | config: this.config,
552 | });
553 |
554 | if (!response || !response.success) {
555 | throw new Error(response?.error || 'Failed to initialize engine in offscreen document');
556 | }
557 |
558 | console.log('SemanticSimilarityEngineProxy: Engine reinitialized successfully');
559 | }
560 | } finally {
561 | this._isEnsuring = false;
562 | }
563 | }
564 |
565 | /**
566 | * Send message to offscreen document with retry mechanism and auto-reinitialization
567 | */
568 | private async sendMessageToOffscreen(message: any, maxRetries: number = 3): Promise<any> {
569 | // 确保offscreen document存在
570 | await this.offscreenManager.ensureOffscreenDocument();
571 |
572 | let lastError: Error | null = null;
573 |
574 | for (let attempt = 1; attempt <= maxRetries; attempt++) {
575 | try {
576 | console.log(
577 | `SemanticSimilarityEngineProxy: Sending message (attempt ${attempt}/${maxRetries}):`,
578 | message.type,
579 | );
580 |
581 | const response = await chrome.runtime.sendMessage(message);
582 |
583 | if (!response) {
584 | throw new Error('No response received from offscreen document');
585 | }
586 |
587 | // If engine not initialized error received, try to reinitialize
588 | if (!response.success && response.error && response.error.includes('not initialized')) {
589 | console.log(
590 | 'SemanticSimilarityEngineProxy: Engine not initialized, attempting to reinitialize...',
591 | );
592 | await this.ensureOffscreenEngineInitialized();
593 |
594 | // Resend original message
595 | const retryResponse = await chrome.runtime.sendMessage(message);
596 | if (retryResponse && retryResponse.success) {
597 | return retryResponse;
598 | }
599 | }
600 |
601 | return response;
602 | } catch (error) {
603 | lastError = error as Error;
604 | console.warn(
605 | `SemanticSimilarityEngineProxy: Message failed (attempt ${attempt}/${maxRetries}):`,
606 | error,
607 | );
608 |
609 | // If engine not initialized error, try to reinitialize
610 | if (error instanceof Error && error.message.includes('not initialized')) {
611 | try {
612 | console.log(
613 | 'SemanticSimilarityEngineProxy: Attempting to reinitialize engine due to error...',
614 | );
615 | await this.ensureOffscreenEngineInitialized();
616 |
617 | // Resend original message
618 | const retryResponse = await chrome.runtime.sendMessage(message);
619 | if (retryResponse && retryResponse.success) {
620 | return retryResponse;
621 | }
622 | } catch (reinitError) {
623 | console.warn(
624 | 'SemanticSimilarityEngineProxy: Failed to reinitialize engine:',
625 | reinitError,
626 | );
627 | }
628 | }
629 |
630 | if (attempt < maxRetries) {
631 | // Wait before retry
632 | await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
633 |
634 | // Re-ensure offscreen document exists
635 | try {
636 | await this.offscreenManager.ensureOffscreenDocument();
637 | } catch (offscreenError) {
638 | console.warn(
639 | 'SemanticSimilarityEngineProxy: Failed to ensure offscreen document:',
640 | offscreenError,
641 | );
642 | }
643 | }
644 | }
645 | }
646 |
647 | throw new Error(
648 | `Failed to communicate with offscreen document after ${maxRetries} attempts. Last error: ${lastError?.message}`,
649 | );
650 | }
651 |
652 | async getEmbedding(text: string, options: Record<string, any> = {}): Promise<Float32Array> {
653 | if (!this._isInitialized) {
654 | await this.initialize();
655 | }
656 |
657 | // Check and ensure engine is initialized before each call
658 | await this.ensureOffscreenEngineInitialized();
659 |
660 | const response = await this.sendMessageToOffscreen({
661 | target: 'offscreen',
662 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_COMPUTE,
663 | text: text,
664 | options: options,
665 | });
666 |
667 | if (!response || !response.success) {
668 | throw new Error(response?.error || 'Failed to get embedding from offscreen document');
669 | }
670 |
671 | if (!response.embedding || !Array.isArray(response.embedding)) {
672 | throw new Error('Invalid embedding data received from offscreen document');
673 | }
674 |
675 | return new Float32Array(response.embedding);
676 | }
677 |
678 | async getEmbeddingsBatch(
679 | texts: string[],
680 | options: Record<string, any> = {},
681 | ): Promise<Float32Array[]> {
682 | if (!this._isInitialized) {
683 | await this.initialize();
684 | }
685 |
686 | if (!texts || texts.length === 0) return [];
687 |
688 | // Check and ensure engine is initialized before each call
689 | await this.ensureOffscreenEngineInitialized();
690 |
691 | const response = await this.sendMessageToOffscreen({
692 | target: 'offscreen',
693 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_BATCH_COMPUTE,
694 | texts: texts,
695 | options: options,
696 | });
697 |
698 | if (!response || !response.success) {
699 | throw new Error(response?.error || 'Failed to get embeddings batch from offscreen document');
700 | }
701 |
702 | return response.embeddings.map((emb: number[]) => new Float32Array(emb));
703 | }
704 |
705 | async computeSimilarity(
706 | text1: string,
707 | text2: string,
708 | options: Record<string, any> = {},
709 | ): Promise<number> {
710 | const [embedding1, embedding2] = await this.getEmbeddingsBatch([text1, text2], options);
711 | return this.cosineSimilarity(embedding1, embedding2);
712 | }
713 |
714 | async computeSimilarityBatch(
715 | pairs: { text1: string; text2: string }[],
716 | options: Record<string, any> = {},
717 | ): Promise<number[]> {
718 | if (!this._isInitialized) {
719 | await this.initialize();
720 | }
721 |
722 | // Check and ensure engine is initialized before each call
723 | await this.ensureOffscreenEngineInitialized();
724 |
725 | const response = await this.sendMessageToOffscreen({
726 | target: 'offscreen',
727 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_BATCH_COMPUTE,
728 | pairs: pairs,
729 | options: options,
730 | });
731 |
732 | if (!response || !response.success) {
733 | throw new Error(
734 | response?.error || 'Failed to compute similarity batch from offscreen document',
735 | );
736 | }
737 |
738 | return response.similarities;
739 | }
740 |
741 | private cosineSimilarity(a: Float32Array, b: Float32Array): number {
742 | if (a.length !== b.length) {
743 | throw new Error(`Vector dimensions don't match: ${a.length} vs ${b.length}`);
744 | }
745 |
746 | let dotProduct = 0;
747 | let normA = 0;
748 | let normB = 0;
749 |
750 | for (let i = 0; i < a.length; i++) {
751 | dotProduct += a[i] * b[i];
752 | normA += a[i] * a[i];
753 | normB += b[i] * b[i];
754 | }
755 |
756 | const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
757 | return magnitude === 0 ? 0 : dotProduct / magnitude;
758 | }
759 |
760 | get isInitialized(): boolean {
761 | return this._isInitialized;
762 | }
763 |
764 | async dispose(): Promise<void> {
765 | // Proxy class doesn't need to clean up resources, actual resources are managed by offscreen
766 | this._isInitialized = false;
767 | console.log('SemanticSimilarityEngineProxy: Proxy disposed');
768 | }
769 | }
770 |
771 | export class SemanticSimilarityEngine {
772 | private worker: Worker | null = null;
773 | private tokenizer: PreTrainedTokenizer | null = null;
774 | public isInitialized = false;
775 | private isInitializing = false;
776 | private initPromise: Promise<void> | null = null;
777 | private nextTokenId = 0;
778 | private pendingMessages = new Map<number, PendingMessage>();
779 | private useOffscreen = false; // Whether to use offscreen mode
780 |
781 | public readonly config: Required<ModelConfig>;
782 |
783 | private embeddingCache: LRUCache<string, Float32Array>;
784 | // Added: tokenization cache
785 | private tokenizationCache: LRUCache<string, TokenizedOutput>;
786 | // Added: memory pool manager
787 | private memoryPool: EmbeddingMemoryPool;
788 | // Added: SIMD math engine
789 | private simdMath: SIMDMathEngine | null = null;
790 | private useSIMD = false;
791 |
792 | public cacheStats = {
793 | embedding: { hits: 0, misses: 0, size: 0 },
794 | tokenization: { hits: 0, misses: 0, size: 0 },
795 | };
796 |
797 | public performanceStats = {
798 | totalEmbeddingComputations: 0,
799 | totalEmbeddingTime: 0,
800 | averageEmbeddingTime: 0,
801 | totalTokenizationTime: 0,
802 | averageTokenizationTime: 0,
803 | totalSimilarityComputations: 0,
804 | totalSimilarityTime: 0,
805 | averageSimilarityTime: 0,
806 | workerStats: null as WorkerStats | null,
807 | };
808 |
809 | private runningWorkerTasks = 0;
810 | private workerTaskQueue: (() => void)[] = [];
811 |
812 | /**
813 | * Detect if current runtime environment supports Worker
814 | */
815 | private isWorkerSupported(): boolean {
816 | try {
817 | // Check if in Service Worker environment (background script)
818 | if (typeof importScripts === 'function') {
819 | return false;
820 | }
821 |
822 | // Check if Worker constructor is available
823 | return typeof Worker !== 'undefined';
824 | } catch {
825 | return false;
826 | }
827 | }
828 |
829 | /**
830 | * Detect if in offscreen document environment
831 | */
832 | private isInOffscreenDocument(): boolean {
833 | try {
834 | // In offscreen document, window.location.pathname is usually '/offscreen.html'
835 | return (
836 | typeof window !== 'undefined' &&
837 | window.location &&
838 | window.location.pathname.includes('offscreen')
839 | );
840 | } catch {
841 | return false;
842 | }
843 | }
844 |
845 | /**
846 | * Ensure offscreen document exists
847 | */
848 | private async ensureOffscreenDocument(): Promise<void> {
849 | return OffscreenManager.getInstance().ensureOffscreenDocument();
850 | }
851 |
852 | // Helper function to safely convert tensor data to number array
853 | private convertTensorDataToNumbers(data: any): number[] {
854 | if (data instanceof BigInt64Array) {
855 | return Array.from(data, (val: bigint) => Number(val));
856 | } else if (data instanceof Int32Array) {
857 | return Array.from(data);
858 | } else {
859 | return Array.from(data);
860 | }
861 | }
862 |
863 | constructor(options: Partial<ModelConfig> = {}) {
864 | console.log('SemanticSimilarityEngine: Constructor called with options:', {
865 | useLocalFiles: options.useLocalFiles,
866 | modelIdentifier: options.modelIdentifier,
867 | forceOffscreen: options.forceOffscreen,
868 | modelPreset: options.modelPreset,
869 | modelVersion: options.modelVersion,
870 | });
871 |
872 | // Handle model presets
873 | let modelConfig = { ...options };
874 | if (options.modelPreset && PREDEFINED_MODELS[options.modelPreset]) {
875 | const preset = PREDEFINED_MODELS[options.modelPreset];
876 | const modelVersion = options.modelVersion || 'quantized'; // Default to quantized version
877 | const baseModelIdentifier = preset.modelIdentifier; // Use base identifier without version suffix
878 | const onnxFileName = getOnnxFileNameForVersion(modelVersion); // Get ONNX filename based on version
879 |
880 | // Get model-specific configuration
881 | const modelSpecificConfig = (preset as any).modelSpecificConfig || {};
882 |
883 | modelConfig = {
884 | ...options,
885 | modelIdentifier: baseModelIdentifier, // Use base identifier
886 | onnxModelFile: onnxFileName, // Set corresponding version ONNX filename
887 | dimension: preset.dimension,
888 | modelVersion: modelVersion,
889 | requiresTokenTypeIds: modelSpecificConfig.requiresTokenTypeIds !== false, // Default to true unless explicitly set to false
890 | };
891 | console.log(
892 | `SemanticSimilarityEngine: Using model preset "${options.modelPreset}" with version "${modelVersion}":`,
893 | preset,
894 | );
895 | console.log(`SemanticSimilarityEngine: Base model identifier: ${baseModelIdentifier}`);
896 | console.log(`SemanticSimilarityEngine: ONNX file for version: ${onnxFileName}`);
897 | console.log(
898 | `SemanticSimilarityEngine: Requires token_type_ids: ${modelConfig.requiresTokenTypeIds}`,
899 | );
900 | }
901 |
902 | // Set default configuration - using 2025 recommended default model
903 | this.config = {
904 | ...modelConfig,
905 | modelIdentifier: modelConfig.modelIdentifier || 'Xenova/bge-small-en-v1.5',
906 | localModelPathPrefix: modelConfig.localModelPathPrefix || 'models/',
907 | onnxModelFile: modelConfig.onnxModelFile || 'model.onnx',
908 | maxLength: modelConfig.maxLength || 256,
909 | cacheSize: modelConfig.cacheSize || 500,
910 | numThreads:
911 | modelConfig.numThreads ||
912 | (typeof navigator !== 'undefined' && navigator.hardwareConcurrency
913 | ? Math.max(1, Math.floor(navigator.hardwareConcurrency / 2))
914 | : 2),
915 | executionProviders:
916 | modelConfig.executionProviders ||
917 | (typeof WebAssembly === 'object' &&
918 | WebAssembly.validate(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]))
919 | ? ['wasm']
920 | : ['webgl']),
921 | useLocalFiles: (() => {
922 | console.log(
923 | 'SemanticSimilarityEngine: DEBUG - modelConfig.useLocalFiles:',
924 | modelConfig.useLocalFiles,
925 | );
926 | console.log(
927 | 'SemanticSimilarityEngine: DEBUG - modelConfig.useLocalFiles !== undefined:',
928 | modelConfig.useLocalFiles !== undefined,
929 | );
930 | const result = modelConfig.useLocalFiles !== undefined ? modelConfig.useLocalFiles : true;
931 | console.log('SemanticSimilarityEngine: DEBUG - final useLocalFiles value:', result);
932 | return result;
933 | })(),
934 | workerPath: modelConfig.workerPath || 'js/similarity.worker.js', // Will be overridden by WXT's `new URL`
935 | concurrentLimit:
936 | modelConfig.concurrentLimit ||
937 | Math.max(
938 | 1,
939 | modelConfig.numThreads ||
940 | (typeof navigator !== 'undefined' && navigator.hardwareConcurrency
941 | ? Math.max(1, Math.floor(navigator.hardwareConcurrency / 2))
942 | : 2),
943 | ),
944 | forceOffscreen: modelConfig.forceOffscreen || false,
945 | modelPreset: modelConfig.modelPreset || 'bge-small-en-v1.5',
946 | dimension: modelConfig.dimension || 384,
947 | modelVersion: modelConfig.modelVersion || 'quantized',
948 | requiresTokenTypeIds: modelConfig.requiresTokenTypeIds !== false, // Default to true
949 | } as Required<ModelConfig>;
950 |
951 | console.log('SemanticSimilarityEngine: Final config:', {
952 | useLocalFiles: this.config.useLocalFiles,
953 | modelIdentifier: this.config.modelIdentifier,
954 | forceOffscreen: this.config.forceOffscreen,
955 | });
956 |
957 | this.embeddingCache = new LRUCache<string, Float32Array>(this.config.cacheSize);
958 | this.tokenizationCache = new LRUCache<string, TokenizedOutput>(
959 | Math.min(this.config.cacheSize, 200),
960 | );
961 | this.memoryPool = new EmbeddingMemoryPool();
962 | this.simdMath = new SIMDMathEngine();
963 | }
964 |
965 | private _sendMessageToWorker(
966 | type: string,
967 | payload?: WorkerMessagePayload,
968 | transferList?: Transferable[],
969 | ): Promise<WorkerResponsePayload> {
970 | return new Promise((resolve, reject) => {
971 | if (!this.worker) {
972 | reject(new Error('Worker is not initialized.'));
973 | return;
974 | }
975 | const id = this.nextTokenId++;
976 | this.pendingMessages.set(id, { resolve, reject, type });
977 |
978 | // Use transferable objects if provided for zero-copy transfer
979 | if (transferList && transferList.length > 0) {
980 | this.worker.postMessage({ id, type, payload }, transferList);
981 | } else {
982 | this.worker.postMessage({ id, type, payload });
983 | }
984 | });
985 | }
986 |
987 | private _setupWorker(): void {
988 | console.log('SemanticSimilarityEngine: Setting up worker...');
989 |
990 | // 方式1: Chrome extension URL (推荐,生产环境最可靠)
991 | try {
992 | const workerUrl = chrome.runtime.getURL('workers/similarity.worker.js');
993 | console.log(`SemanticSimilarityEngine: Trying chrome.runtime.getURL ${workerUrl}`);
994 | this.worker = new Worker(workerUrl);
995 | console.log(`SemanticSimilarityEngine: Method 1 successful with path`);
996 | } catch (error) {
997 | console.warn('Method (chrome.runtime.getURL) failed:', error);
998 | }
999 |
1000 | if (!this.worker) {
1001 | throw new Error('Worker creation failed');
1002 | }
1003 |
1004 | this.worker.onmessage = (
1005 | event: MessageEvent<{
1006 | id: number;
1007 | type: string;
1008 | status: string;
1009 | payload: WorkerResponsePayload;
1010 | stats?: WorkerStats;
1011 | }>,
1012 | ) => {
1013 | const { id, status, payload, stats } = event.data;
1014 | const promiseCallbacks = this.pendingMessages.get(id);
1015 | if (!promiseCallbacks) return;
1016 |
1017 | this.pendingMessages.delete(id);
1018 |
1019 | // 更新 Worker 统计信息
1020 | if (stats) {
1021 | this.performanceStats.workerStats = stats;
1022 | }
1023 |
1024 | if (status === 'success') {
1025 | promiseCallbacks.resolve(payload);
1026 | } else {
1027 | const error = new Error(
1028 | payload?.message || `Worker error for task ${promiseCallbacks.type}`,
1029 | );
1030 | (error as any).name = (payload as any)?.name || 'WorkerError';
1031 | (error as any).stack = (payload as any)?.stack || undefined;
1032 | console.error(
1033 | `Error from worker (task ${id}, type ${promiseCallbacks.type}):`,
1034 | error,
1035 | event.data,
1036 | );
1037 | promiseCallbacks.reject(error);
1038 | }
1039 | };
1040 |
1041 | this.worker.onerror = (error: ErrorEvent) => {
1042 | console.error('==== Unhandled error in SemanticSimilarityEngine Worker ====');
1043 | console.error('Event Message:', error.message);
1044 | console.error('Event Filename:', error.filename);
1045 | console.error('Event Lineno:', error.lineno);
1046 | console.error('Event Colno:', error.colno);
1047 | if (error.error) {
1048 | // 检查 event.error 是否存在
1049 | console.error('Actual Error Name:', error.error.name);
1050 | console.error('Actual Error Message:', error.error.message);
1051 | console.error('Actual Error Stack:', error.error.stack);
1052 | } else {
1053 | console.error('Actual Error object (event.error) is not available. Error details:', {
1054 | message: error.message,
1055 | filename: error.filename,
1056 | lineno: error.lineno,
1057 | colno: error.colno,
1058 | });
1059 | }
1060 | console.error('==========================================================');
1061 | this.pendingMessages.forEach((callbacks) => {
1062 | callbacks.reject(new Error(`Worker terminated or unhandled error: ${error.message}`));
1063 | });
1064 | this.pendingMessages.clear();
1065 | this.isInitialized = false;
1066 | this.isInitializing = false;
1067 | };
1068 | }
1069 |
1070 | public async initialize(): Promise<void> {
1071 | if (this.isInitialized) return Promise.resolve();
1072 | if (this.isInitializing && this.initPromise) return this.initPromise;
1073 |
1074 | this.isInitializing = true;
1075 | this.initPromise = this._doInitialize().finally(() => {
1076 | this.isInitializing = false;
1077 | // this.warmupModel();
1078 | });
1079 | return this.initPromise;
1080 | }
1081 |
1082 | /**
1083 | * 带进度回调的初始化方法
1084 | */
1085 | public async initializeWithProgress(
1086 | onProgress?: (progress: { status: string; progress: number; message?: string }) => void,
1087 | ): Promise<void> {
1088 | if (this.isInitialized) return Promise.resolve();
1089 | if (this.isInitializing && this.initPromise) return this.initPromise;
1090 |
1091 | this.isInitializing = true;
1092 | this.initPromise = this._doInitializeWithProgress(onProgress).finally(() => {
1093 | this.isInitializing = false;
1094 | // this.warmupModel();
1095 | });
1096 | return this.initPromise;
1097 | }
1098 |
1099 | /**
1100 | * 带进度回调的内部初始化方法
1101 | */
1102 | private async _doInitializeWithProgress(
1103 | onProgress?: (progress: { status: string; progress: number; message?: string }) => void,
1104 | ): Promise<void> {
1105 | console.log('SemanticSimilarityEngine: Initializing with progress tracking...');
1106 | const startTime = performance.now();
1107 |
1108 | // 进度报告辅助函数
1109 | const reportProgress = (status: string, progress: number, message?: string) => {
1110 | if (onProgress) {
1111 | onProgress({ status, progress, message });
1112 | }
1113 | };
1114 |
1115 | try {
1116 | reportProgress('initializing', 5, 'Starting initialization...');
1117 |
1118 | // 检测环境并决定使用哪种模式
1119 | const workerSupported = this.isWorkerSupported();
1120 | const inOffscreenDocument = this.isInOffscreenDocument();
1121 |
1122 | // 🛠️ 防止死循环:如果已经在 offscreen document 中,强制使用直接 Worker 模式
1123 | if (inOffscreenDocument) {
1124 | this.useOffscreen = false;
1125 | console.log(
1126 | 'SemanticSimilarityEngine: Running in offscreen document, using direct Worker mode to prevent recursion',
1127 | );
1128 | } else {
1129 | this.useOffscreen = this.config.forceOffscreen || !workerSupported;
1130 | }
1131 |
1132 | console.log(
1133 | `SemanticSimilarityEngine: Worker supported: ${workerSupported}, In offscreen: ${inOffscreenDocument}, Using offscreen: ${this.useOffscreen}`,
1134 | );
1135 |
1136 | reportProgress('initializing', 10, 'Environment detection complete');
1137 |
1138 | if (this.useOffscreen) {
1139 | // 使用offscreen模式 - 委托给offscreen document,它会处理自己的进度
1140 | reportProgress('initializing', 15, 'Setting up offscreen document...');
1141 | await this.ensureOffscreenDocument();
1142 |
1143 | // 发送初始化消息到offscreen document
1144 | console.log('SemanticSimilarityEngine: Sending config to offscreen:', {
1145 | useLocalFiles: this.config.useLocalFiles,
1146 | modelIdentifier: this.config.modelIdentifier,
1147 | localModelPathPrefix: this.config.localModelPathPrefix,
1148 | });
1149 |
1150 | // 确保配置对象被正确序列化,显式设置所有属性
1151 | const configToSend = {
1152 | modelIdentifier: this.config.modelIdentifier,
1153 | localModelPathPrefix: this.config.localModelPathPrefix,
1154 | onnxModelFile: this.config.onnxModelFile,
1155 | maxLength: this.config.maxLength,
1156 | cacheSize: this.config.cacheSize,
1157 | numThreads: this.config.numThreads,
1158 | executionProviders: this.config.executionProviders,
1159 | useLocalFiles: Boolean(this.config.useLocalFiles), // 强制转换为布尔值
1160 | workerPath: this.config.workerPath,
1161 | concurrentLimit: this.config.concurrentLimit,
1162 | forceOffscreen: this.config.forceOffscreen,
1163 | modelPreset: this.config.modelPreset,
1164 | modelVersion: this.config.modelVersion,
1165 | dimension: this.config.dimension,
1166 | };
1167 |
1168 | // 使用 JSON 序列化确保数据完整性
1169 | const serializedConfig = JSON.parse(JSON.stringify(configToSend));
1170 |
1171 | reportProgress('initializing', 20, 'Delegating to offscreen document...');
1172 |
1173 | const response = await chrome.runtime.sendMessage({
1174 | target: 'offscreen',
1175 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
1176 | config: serializedConfig,
1177 | });
1178 |
1179 | if (!response || !response.success) {
1180 | throw new Error(response?.error || 'Failed to initialize engine in offscreen document');
1181 | }
1182 |
1183 | reportProgress('ready', 100, 'Initialized via offscreen document');
1184 | console.log('SemanticSimilarityEngine: Initialized via offscreen document');
1185 | } else {
1186 | // 使用直接Worker模式 - 这里我们可以提供真实的进度跟踪
1187 | await this._initializeDirectWorkerWithProgress(reportProgress);
1188 | }
1189 |
1190 | this.isInitialized = true;
1191 | console.log(
1192 | `SemanticSimilarityEngine: Initialization complete in ${(performance.now() - startTime).toFixed(2)}ms`,
1193 | );
1194 | } catch (error) {
1195 | console.error('SemanticSimilarityEngine: Initialization failed.', error);
1196 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1197 | reportProgress('error', 0, `Initialization failed: ${errorMessage}`);
1198 | if (this.worker) this.worker.terminate();
1199 | this.worker = null;
1200 | this.isInitialized = false;
1201 | this.isInitializing = false;
1202 | this.initPromise = null;
1203 |
1204 | // 创建一个更详细的错误对象
1205 | const enhancedError = new Error(errorMessage);
1206 | enhancedError.name = 'ModelInitializationError';
1207 | throw enhancedError;
1208 | }
1209 | }
1210 |
1211 | private async _doInitialize(): Promise<void> {
1212 | console.log('SemanticSimilarityEngine: Initializing...');
1213 | const startTime = performance.now();
1214 | try {
1215 | // 检测环境并决定使用哪种模式
1216 | const workerSupported = this.isWorkerSupported();
1217 | const inOffscreenDocument = this.isInOffscreenDocument();
1218 |
1219 | // 🛠️ 防止死循环:如果已经在 offscreen document 中,强制使用直接 Worker 模式
1220 | if (inOffscreenDocument) {
1221 | this.useOffscreen = false;
1222 | console.log(
1223 | 'SemanticSimilarityEngine: Running in offscreen document, using direct Worker mode to prevent recursion',
1224 | );
1225 | } else {
1226 | this.useOffscreen = this.config.forceOffscreen || !workerSupported;
1227 | }
1228 |
1229 | console.log(
1230 | `SemanticSimilarityEngine: Worker supported: ${workerSupported}, In offscreen: ${inOffscreenDocument}, Using offscreen: ${this.useOffscreen}`,
1231 | );
1232 |
1233 | if (this.useOffscreen) {
1234 | // 使用offscreen模式
1235 | await this.ensureOffscreenDocument();
1236 |
1237 | // 发送初始化消息到offscreen document
1238 | console.log('SemanticSimilarityEngine: Sending config to offscreen:', {
1239 | useLocalFiles: this.config.useLocalFiles,
1240 | modelIdentifier: this.config.modelIdentifier,
1241 | localModelPathPrefix: this.config.localModelPathPrefix,
1242 | });
1243 |
1244 | // 确保配置对象被正确序列化,显式设置所有属性
1245 | const configToSend = {
1246 | modelIdentifier: this.config.modelIdentifier,
1247 | localModelPathPrefix: this.config.localModelPathPrefix,
1248 | onnxModelFile: this.config.onnxModelFile,
1249 | maxLength: this.config.maxLength,
1250 | cacheSize: this.config.cacheSize,
1251 | numThreads: this.config.numThreads,
1252 | executionProviders: this.config.executionProviders,
1253 | useLocalFiles: Boolean(this.config.useLocalFiles), // 强制转换为布尔值
1254 | workerPath: this.config.workerPath,
1255 | concurrentLimit: this.config.concurrentLimit,
1256 | forceOffscreen: this.config.forceOffscreen,
1257 | modelPreset: this.config.modelPreset,
1258 | modelVersion: this.config.modelVersion,
1259 | dimension: this.config.dimension,
1260 | };
1261 |
1262 | console.log(
1263 | 'SemanticSimilarityEngine: DEBUG - configToSend.useLocalFiles:',
1264 | configToSend.useLocalFiles,
1265 | );
1266 | console.log(
1267 | 'SemanticSimilarityEngine: DEBUG - typeof configToSend.useLocalFiles:',
1268 | typeof configToSend.useLocalFiles,
1269 | );
1270 |
1271 | console.log('SemanticSimilarityEngine: Explicit config to send:', configToSend);
1272 | console.log(
1273 | 'SemanticSimilarityEngine: DEBUG - this.config.useLocalFiles value:',
1274 | this.config.useLocalFiles,
1275 | );
1276 | console.log(
1277 | 'SemanticSimilarityEngine: DEBUG - typeof this.config.useLocalFiles:',
1278 | typeof this.config.useLocalFiles,
1279 | );
1280 |
1281 | // 使用 JSON 序列化确保数据完整性
1282 | const serializedConfig = JSON.parse(JSON.stringify(configToSend));
1283 | console.log(
1284 | 'SemanticSimilarityEngine: DEBUG - serializedConfig.useLocalFiles:',
1285 | serializedConfig.useLocalFiles,
1286 | );
1287 |
1288 | const response = await chrome.runtime.sendMessage({
1289 | target: 'offscreen',
1290 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
1291 | config: serializedConfig, // 使用原始配置,不强制修改 useLocalFiles
1292 | });
1293 |
1294 | if (!response || !response.success) {
1295 | throw new Error(response?.error || 'Failed to initialize engine in offscreen document');
1296 | }
1297 |
1298 | console.log('SemanticSimilarityEngine: Initialized via offscreen document');
1299 | } else {
1300 | // 使用直接Worker模式
1301 | this._setupWorker();
1302 |
1303 | TransformersEnv.allowRemoteModels = !this.config.useLocalFiles;
1304 | TransformersEnv.allowLocalModels = this.config.useLocalFiles;
1305 |
1306 | console.log(`SemanticSimilarityEngine: TransformersEnv config:`, {
1307 | allowRemoteModels: TransformersEnv.allowRemoteModels,
1308 | allowLocalModels: TransformersEnv.allowLocalModels,
1309 | useLocalFiles: this.config.useLocalFiles,
1310 | });
1311 | if (TransformersEnv.backends?.onnx?.wasm) {
1312 | // 检查路径是否存在
1313 | TransformersEnv.backends.onnx.wasm.numThreads = this.config.numThreads;
1314 | }
1315 |
1316 | let tokenizerIdentifier = this.config.modelIdentifier;
1317 | if (this.config.useLocalFiles) {
1318 | // 对于WXT,public目录下的资源在运行时位于根路径
1319 | // 直接使用模型标识符,transformers.js 会自动添加 /models/ 前缀
1320 | tokenizerIdentifier = this.config.modelIdentifier;
1321 | }
1322 | console.log(
1323 | `SemanticSimilarityEngine: Loading tokenizer from ${tokenizerIdentifier} (local_files_only: ${this.config.useLocalFiles})`,
1324 | );
1325 | const tokenizerConfig: any = {
1326 | quantized: false,
1327 | local_files_only: this.config.useLocalFiles,
1328 | };
1329 |
1330 | // 对于不需要token_type_ids的模型,在tokenizer配置中明确设置
1331 | if (!this.config.requiresTokenTypeIds) {
1332 | tokenizerConfig.return_token_type_ids = false;
1333 | }
1334 |
1335 | console.log(`SemanticSimilarityEngine: Full tokenizer config:`, {
1336 | tokenizerIdentifier,
1337 | localModelPathPrefix: this.config.localModelPathPrefix,
1338 | modelIdentifier: this.config.modelIdentifier,
1339 | useLocalFiles: this.config.useLocalFiles,
1340 | local_files_only: this.config.useLocalFiles,
1341 | requiresTokenTypeIds: this.config.requiresTokenTypeIds,
1342 | tokenizerConfig,
1343 | });
1344 | this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerIdentifier, tokenizerConfig);
1345 | console.log('SemanticSimilarityEngine: Tokenizer loaded.');
1346 |
1347 | if (this.config.useLocalFiles) {
1348 | // Local files mode - use URL path as before
1349 | const onnxModelPathForWorker = chrome.runtime.getURL(
1350 | `models/${this.config.modelIdentifier}/${this.config.onnxModelFile}`,
1351 | );
1352 | console.log(
1353 | `SemanticSimilarityEngine: Instructing worker to load local ONNX model from ${onnxModelPathForWorker}`,
1354 | );
1355 | await this._sendMessageToWorker('init', {
1356 | modelPath: onnxModelPathForWorker,
1357 | numThreads: this.config.numThreads,
1358 | executionProviders: this.config.executionProviders,
1359 | });
1360 | } else {
1361 | // Remote files mode - use cached model data
1362 | const modelIdParts = this.config.modelIdentifier.split('/');
1363 | const modelNameForUrl =
1364 | modelIdParts.length > 1
1365 | ? this.config.modelIdentifier
1366 | : `Xenova/${this.config.modelIdentifier}`;
1367 | const onnxModelUrl = `https://huggingface.co/${modelNameForUrl}/resolve/main/onnx/${this.config.onnxModelFile}`;
1368 |
1369 | if (!this.config.modelIdentifier.includes('/')) {
1370 | console.warn(
1371 | `Warning: modelIdentifier "${this.config.modelIdentifier}" might not be a full HuggingFace path. Assuming Xenova prefix for remote URL.`,
1372 | );
1373 | }
1374 |
1375 | console.log(`SemanticSimilarityEngine: Getting cached model data from ${onnxModelUrl}`);
1376 |
1377 | // Get model data from cache (may download if not cached)
1378 | const modelData = await getCachedModelData(onnxModelUrl);
1379 |
1380 | console.log(
1381 | `SemanticSimilarityEngine: Sending cached model data to worker (${modelData.byteLength} bytes)`,
1382 | );
1383 |
1384 | // Send ArrayBuffer to worker with transferable objects for zero-copy
1385 | await this._sendMessageToWorker(
1386 | 'init',
1387 | {
1388 | modelData: modelData,
1389 | numThreads: this.config.numThreads,
1390 | executionProviders: this.config.executionProviders,
1391 | },
1392 | [modelData],
1393 | );
1394 | }
1395 | console.log('SemanticSimilarityEngine: Worker reported model initialized.');
1396 |
1397 | // 尝试初始化 SIMD 加速
1398 | try {
1399 | console.log('SemanticSimilarityEngine: Checking SIMD support...');
1400 | const simdSupported = await SIMDMathEngine.checkSIMDSupport();
1401 |
1402 | if (simdSupported) {
1403 | console.log('SemanticSimilarityEngine: SIMD supported, initializing...');
1404 | await this.simdMath!.initialize();
1405 | this.useSIMD = true;
1406 | console.log('SemanticSimilarityEngine: ✅ SIMD acceleration enabled');
1407 | } else {
1408 | console.log(
1409 | 'SemanticSimilarityEngine: ❌ SIMD not supported, using JavaScript fallback',
1410 | );
1411 | console.log('SemanticSimilarityEngine: To enable SIMD, please use:');
1412 | console.log(' - Chrome 91+ (May 2021)');
1413 | console.log(' - Firefox 89+ (June 2021)');
1414 | console.log(' - Safari 16.4+ (March 2023)');
1415 | console.log(' - Edge 91+ (May 2021)');
1416 | this.useSIMD = false;
1417 | }
1418 | } catch (simdError) {
1419 | console.warn(
1420 | 'SemanticSimilarityEngine: SIMD initialization failed, using JavaScript fallback:',
1421 | simdError,
1422 | );
1423 | this.useSIMD = false;
1424 | }
1425 | }
1426 |
1427 | this.isInitialized = true;
1428 | console.log(
1429 | `SemanticSimilarityEngine: Initialization complete in ${(performance.now() - startTime).toFixed(2)}ms`,
1430 | );
1431 | } catch (error) {
1432 | console.error('SemanticSimilarityEngine: Initialization failed.', error);
1433 | if (this.worker) this.worker.terminate();
1434 | this.worker = null;
1435 | this.isInitialized = false;
1436 | this.isInitializing = false;
1437 | this.initPromise = null;
1438 |
1439 | // 创建一个更详细的错误对象
1440 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1441 | const enhancedError = new Error(errorMessage);
1442 | enhancedError.name = 'ModelInitializationError';
1443 | throw enhancedError;
1444 | }
1445 | }
1446 |
1447 | /**
1448 | * 直接Worker模式的初始化,支持进度回调
1449 | */
1450 | private async _initializeDirectWorkerWithProgress(
1451 | reportProgress: (status: string, progress: number, message?: string) => void,
1452 | ): Promise<void> {
1453 | // 使用直接Worker模式
1454 | reportProgress('initializing', 25, 'Setting up worker...');
1455 | this._setupWorker();
1456 |
1457 | TransformersEnv.allowRemoteModels = !this.config.useLocalFiles;
1458 | TransformersEnv.allowLocalModels = this.config.useLocalFiles;
1459 |
1460 | console.log(`SemanticSimilarityEngine: TransformersEnv config:`, {
1461 | allowRemoteModels: TransformersEnv.allowRemoteModels,
1462 | allowLocalModels: TransformersEnv.allowLocalModels,
1463 | useLocalFiles: this.config.useLocalFiles,
1464 | });
1465 | if (TransformersEnv.backends?.onnx?.wasm) {
1466 | TransformersEnv.backends.onnx.wasm.numThreads = this.config.numThreads;
1467 | }
1468 |
1469 | let tokenizerIdentifier = this.config.modelIdentifier;
1470 | if (this.config.useLocalFiles) {
1471 | tokenizerIdentifier = this.config.modelIdentifier;
1472 | }
1473 |
1474 | reportProgress('downloading', 40, 'Loading tokenizer...');
1475 | console.log(
1476 | `SemanticSimilarityEngine: Loading tokenizer from ${tokenizerIdentifier} (local_files_only: ${this.config.useLocalFiles})`,
1477 | );
1478 |
1479 | // 使用 transformers.js 2.17+ 的进度回调功能
1480 | const tokenizerProgressCallback = (progress: any) => {
1481 | if (progress.status === 'downloading') {
1482 | const progressPercent = Math.min(40 + (progress.progress || 0) * 0.3, 70);
1483 | reportProgress(
1484 | 'downloading',
1485 | progressPercent,
1486 | `Downloading tokenizer: ${progress.file || ''}`,
1487 | );
1488 | }
1489 | };
1490 |
1491 | const tokenizerConfig: any = {
1492 | quantized: false,
1493 | local_files_only: this.config.useLocalFiles,
1494 | };
1495 |
1496 | // 对于不需要token_type_ids的模型,在tokenizer配置中明确设置
1497 | if (!this.config.requiresTokenTypeIds) {
1498 | tokenizerConfig.return_token_type_ids = false;
1499 | }
1500 |
1501 | try {
1502 | if (!this.config.useLocalFiles) {
1503 | tokenizerConfig.progress_callback = tokenizerProgressCallback;
1504 | }
1505 | this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerIdentifier, tokenizerConfig);
1506 | } catch (error) {
1507 | // 如果进度回调不支持,回退到标准方式
1508 | console.log(
1509 | 'SemanticSimilarityEngine: Progress callback not supported, using standard loading',
1510 | );
1511 | delete tokenizerConfig.progress_callback;
1512 | this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerIdentifier, tokenizerConfig);
1513 | }
1514 |
1515 | reportProgress('downloading', 70, 'Tokenizer loaded, setting up ONNX model...');
1516 | console.log('SemanticSimilarityEngine: Tokenizer loaded.');
1517 |
1518 | if (this.config.useLocalFiles) {
1519 | // Local files mode - use URL path as before
1520 | const onnxModelPathForWorker = chrome.runtime.getURL(
1521 | `models/${this.config.modelIdentifier}/${this.config.onnxModelFile}`,
1522 | );
1523 | reportProgress('downloading', 80, 'Loading local ONNX model...');
1524 | console.log(
1525 | `SemanticSimilarityEngine: Instructing worker to load local ONNX model from ${onnxModelPathForWorker}`,
1526 | );
1527 | await this._sendMessageToWorker('init', {
1528 | modelPath: onnxModelPathForWorker,
1529 | numThreads: this.config.numThreads,
1530 | executionProviders: this.config.executionProviders,
1531 | });
1532 | } else {
1533 | // Remote files mode - use cached model data
1534 | const modelIdParts = this.config.modelIdentifier.split('/');
1535 | const modelNameForUrl =
1536 | modelIdParts.length > 1
1537 | ? this.config.modelIdentifier
1538 | : `Xenova/${this.config.modelIdentifier}`;
1539 | const onnxModelUrl = `https://huggingface.co/${modelNameForUrl}/resolve/main/onnx/${this.config.onnxModelFile}`;
1540 |
1541 | if (!this.config.modelIdentifier.includes('/')) {
1542 | console.warn(
1543 | `Warning: modelIdentifier "${this.config.modelIdentifier}" might not be a full HuggingFace path. Assuming Xenova prefix for remote URL.`,
1544 | );
1545 | }
1546 |
1547 | reportProgress('downloading', 80, 'Loading cached ONNX model...');
1548 | console.log(`SemanticSimilarityEngine: Getting cached model data from ${onnxModelUrl}`);
1549 |
1550 | // Get model data from cache (may download if not cached)
1551 | const modelData = await getCachedModelData(onnxModelUrl);
1552 |
1553 | console.log(
1554 | `SemanticSimilarityEngine: Sending cached model data to worker (${modelData.byteLength} bytes)`,
1555 | );
1556 |
1557 | // Send ArrayBuffer to worker with transferable objects for zero-copy
1558 | await this._sendMessageToWorker(
1559 | 'init',
1560 | {
1561 | modelData: modelData,
1562 | numThreads: this.config.numThreads,
1563 | executionProviders: this.config.executionProviders,
1564 | },
1565 | [modelData],
1566 | );
1567 | }
1568 | console.log('SemanticSimilarityEngine: Worker reported model initialized.');
1569 |
1570 | reportProgress('initializing', 90, 'Setting up SIMD acceleration...');
1571 | // 尝试初始化 SIMD 加速
1572 | try {
1573 | console.log('SemanticSimilarityEngine: Checking SIMD support...');
1574 | const simdSupported = await SIMDMathEngine.checkSIMDSupport();
1575 |
1576 | if (simdSupported) {
1577 | console.log('SemanticSimilarityEngine: SIMD supported, initializing...');
1578 | await this.simdMath!.initialize();
1579 | this.useSIMD = true;
1580 | console.log('SemanticSimilarityEngine: ✅ SIMD acceleration enabled');
1581 | } else {
1582 | console.log('SemanticSimilarityEngine: ❌ SIMD not supported, using JavaScript fallback');
1583 | this.useSIMD = false;
1584 | }
1585 | } catch (simdError) {
1586 | console.warn(
1587 | 'SemanticSimilarityEngine: SIMD initialization failed, using JavaScript fallback:',
1588 | simdError,
1589 | );
1590 | this.useSIMD = false;
1591 | }
1592 |
1593 | reportProgress('ready', 100, 'Initialization complete');
1594 | }
1595 |
1596 | public async warmupModel(): Promise<void> {
1597 | if (!this.isInitialized && !this.isInitializing) {
1598 | await this.initialize();
1599 | } else if (this.isInitializing && this.initPromise) {
1600 | await this.initPromise;
1601 | }
1602 | if (!this.isInitialized) throw new Error('Engine not initialized after warmup attempt.');
1603 | console.log('SemanticSimilarityEngine: Warming up model...');
1604 |
1605 | // 更有代表性的预热文本,包含不同长度和语言
1606 | const warmupTexts = [
1607 | // 短文本
1608 | 'Hello',
1609 | '你好',
1610 | 'Test',
1611 | // 中等长度文本
1612 | 'Hello world, this is a test.',
1613 | '你好世界,这是一个测试。',
1614 | 'The quick brown fox jumps over the lazy dog.',
1615 | // 长文本
1616 | 'This is a longer text that contains multiple sentences. It helps warm up the model for various text lengths.',
1617 | '这是一个包含多个句子的较长文本。它有助于为各种文本长度预热模型。',
1618 | ];
1619 |
1620 | try {
1621 | // 渐进式预热:先单个,再批量
1622 | console.log('SemanticSimilarityEngine: Phase 1 - Individual warmup...');
1623 | for (const text of warmupTexts.slice(0, 4)) {
1624 | await this.getEmbedding(text);
1625 | }
1626 |
1627 | console.log('SemanticSimilarityEngine: Phase 2 - Batch warmup...');
1628 | await this.getEmbeddingsBatch(warmupTexts.slice(4));
1629 |
1630 | // 保留预热结果,不清空缓存
1631 | console.log('SemanticSimilarityEngine: Model warmup complete. Cache preserved.');
1632 | console.log(`Embedding cache: ${this.cacheStats.embedding.size} items`);
1633 | console.log(`Tokenization cache: ${this.cacheStats.tokenization.size} items`);
1634 | } catch (error) {
1635 | console.warn('SemanticSimilarityEngine: Warmup failed. This might not be critical.', error);
1636 | }
1637 | }
1638 |
1639 | private async _tokenizeText(text: string | string[]): Promise<TokenizedOutput> {
1640 | if (!this.tokenizer) throw new Error('Tokenizer not initialized.');
1641 |
1642 | // 对于单个文本,尝试使用缓存
1643 | if (typeof text === 'string') {
1644 | const cacheKey = `tokenize:${text}`;
1645 | const cached = this.tokenizationCache.get(cacheKey);
1646 | if (cached) {
1647 | this.cacheStats.tokenization.hits++;
1648 | this.cacheStats.tokenization.size = this.tokenizationCache.size;
1649 | return cached;
1650 | }
1651 | this.cacheStats.tokenization.misses++;
1652 |
1653 | const startTime = performance.now();
1654 | const tokenizerOptions: any = {
1655 | padding: true,
1656 | truncation: true,
1657 | max_length: this.config.maxLength,
1658 | return_tensors: 'np',
1659 | };
1660 |
1661 | // 对于不需要token_type_ids的模型,明确设置return_token_type_ids为false
1662 | if (!this.config.requiresTokenTypeIds) {
1663 | tokenizerOptions.return_token_type_ids = false;
1664 | }
1665 |
1666 | const result = (await this.tokenizer(text, tokenizerOptions)) as TokenizedOutput;
1667 |
1668 | // 更新性能统计
1669 | this.performanceStats.totalTokenizationTime += performance.now() - startTime;
1670 | this.performanceStats.averageTokenizationTime =
1671 | this.performanceStats.totalTokenizationTime /
1672 | (this.cacheStats.tokenization.hits + this.cacheStats.tokenization.misses);
1673 |
1674 | // 缓存结果
1675 | this.tokenizationCache.set(cacheKey, result);
1676 | this.cacheStats.tokenization.size = this.tokenizationCache.size;
1677 |
1678 | return result;
1679 | }
1680 |
1681 | // 对于批量文本,直接处理(批量处理通常不重复)
1682 | const startTime = performance.now();
1683 | const tokenizerOptions: any = {
1684 | padding: true,
1685 | truncation: true,
1686 | max_length: this.config.maxLength,
1687 | return_tensors: 'np',
1688 | };
1689 |
1690 | // 对于不需要token_type_ids的模型,明确设置return_token_type_ids为false
1691 | if (!this.config.requiresTokenTypeIds) {
1692 | tokenizerOptions.return_token_type_ids = false;
1693 | }
1694 |
1695 | const result = (await this.tokenizer(text, tokenizerOptions)) as TokenizedOutput;
1696 |
1697 | this.performanceStats.totalTokenizationTime += performance.now() - startTime;
1698 | return result;
1699 | }
1700 |
1701 | private _extractEmbeddingFromWorkerOutput(
1702 | workerOutput: WorkerResponsePayload,
1703 | attentionMaskArray: number[],
1704 | ): Float32Array {
1705 | if (!workerOutput.data || !workerOutput.dims)
1706 | throw new Error('Invalid worker output for embedding extraction.');
1707 |
1708 | // 优化:直接使用 Float32Array,避免不必要的转换
1709 | const lastHiddenStateData =
1710 | workerOutput.data instanceof Float32Array
1711 | ? workerOutput.data
1712 | : new Float32Array(workerOutput.data);
1713 |
1714 | const dims = workerOutput.dims;
1715 | const seqLength = dims[1];
1716 | const hiddenSize = dims[2];
1717 |
1718 | // 使用内存池获取 embedding 数组
1719 | const embedding = this.memoryPool.getEmbedding(hiddenSize);
1720 | let validTokens = 0;
1721 |
1722 | for (let i = 0; i < seqLength; i++) {
1723 | if (attentionMaskArray[i] === 1) {
1724 | const offset = i * hiddenSize;
1725 | for (let j = 0; j < hiddenSize; j++) {
1726 | embedding[j] += lastHiddenStateData[offset + j];
1727 | }
1728 | validTokens++;
1729 | }
1730 | }
1731 | if (validTokens > 0) {
1732 | for (let i = 0; i < hiddenSize; i++) {
1733 | embedding[i] /= validTokens;
1734 | }
1735 | }
1736 | return this.normalizeVector(embedding);
1737 | }
1738 |
1739 | private _extractBatchEmbeddingsFromWorkerOutput(
1740 | workerOutput: WorkerResponsePayload,
1741 | attentionMasksBatch: number[][],
1742 | ): Float32Array[] {
1743 | if (!workerOutput.data || !workerOutput.dims)
1744 | throw new Error('Invalid worker output for batch embedding extraction.');
1745 |
1746 | // 优化:直接使用 Float32Array,避免不必要的转换
1747 | const lastHiddenStateData =
1748 | workerOutput.data instanceof Float32Array
1749 | ? workerOutput.data
1750 | : new Float32Array(workerOutput.data);
1751 |
1752 | const dims = workerOutput.dims;
1753 | const batchSize = dims[0];
1754 | const seqLength = dims[1];
1755 | const hiddenSize = dims[2];
1756 | const embeddings: Float32Array[] = [];
1757 |
1758 | for (let b = 0; b < batchSize; b++) {
1759 | // 使用内存池获取 embedding 数组
1760 | const embedding = this.memoryPool.getEmbedding(hiddenSize);
1761 | let validTokens = 0;
1762 | const currentAttentionMask = attentionMasksBatch[b];
1763 | for (let i = 0; i < seqLength; i++) {
1764 | if (currentAttentionMask[i] === 1) {
1765 | const offset = (b * seqLength + i) * hiddenSize;
1766 | for (let j = 0; j < hiddenSize; j++) {
1767 | embedding[j] += lastHiddenStateData[offset + j];
1768 | }
1769 | validTokens++;
1770 | }
1771 | }
1772 | if (validTokens > 0) {
1773 | for (let i = 0; i < hiddenSize; i++) {
1774 | embedding[i] /= validTokens;
1775 | }
1776 | }
1777 | embeddings.push(this.normalizeVector(embedding));
1778 | }
1779 | return embeddings;
1780 | }
1781 |
1782 | public async getEmbedding(
1783 | text: string,
1784 | options: Record<string, any> = {},
1785 | ): Promise<Float32Array> {
1786 | if (!this.isInitialized) await this.initialize();
1787 |
1788 | const cacheKey = this.getCacheKey(text, options);
1789 | const cached = this.embeddingCache.get(cacheKey);
1790 | if (cached) {
1791 | this.cacheStats.embedding.hits++;
1792 | this.cacheStats.embedding.size = this.embeddingCache.size;
1793 | return cached;
1794 | }
1795 | this.cacheStats.embedding.misses++;
1796 |
1797 | // 如果使用offscreen模式,委托给offscreen document
1798 | if (this.useOffscreen) {
1799 | const response = await chrome.runtime.sendMessage({
1800 | target: 'offscreen',
1801 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_COMPUTE,
1802 | text: text,
1803 | options: options,
1804 | });
1805 |
1806 | if (!response || !response.success) {
1807 | throw new Error(response?.error || 'Failed to get embedding from offscreen document');
1808 | }
1809 |
1810 | // 验证响应数据
1811 | if (!response.embedding || !Array.isArray(response.embedding)) {
1812 | throw new Error('Invalid embedding data received from offscreen document');
1813 | }
1814 |
1815 | console.log('SemanticSimilarityEngine: Received embedding from offscreen:', {
1816 | length: response.embedding.length,
1817 | type: typeof response.embedding,
1818 | isArray: Array.isArray(response.embedding),
1819 | firstFewValues: response.embedding.slice(0, 5),
1820 | });
1821 |
1822 | const embedding = new Float32Array(response.embedding);
1823 |
1824 | // 验证转换后的数据
1825 | console.log('SemanticSimilarityEngine: Converted embedding:', {
1826 | length: embedding.length,
1827 | type: typeof embedding,
1828 | constructor: embedding.constructor.name,
1829 | isFloat32Array: embedding instanceof Float32Array,
1830 | firstFewValues: Array.from(embedding.slice(0, 5)),
1831 | });
1832 |
1833 | this.embeddingCache.set(cacheKey, embedding);
1834 | this.cacheStats.embedding.size = this.embeddingCache.size;
1835 |
1836 | // 更新性能统计
1837 | this.performanceStats.totalEmbeddingComputations++;
1838 |
1839 | return embedding;
1840 | }
1841 |
1842 | if (this.runningWorkerTasks >= this.config.concurrentLimit) {
1843 | await this.waitForWorkerSlot();
1844 | }
1845 | this.runningWorkerTasks++;
1846 |
1847 | const startTime = performance.now();
1848 | try {
1849 | const tokenized = await this._tokenizeText(text);
1850 |
1851 | const inputIdsData = this.convertTensorDataToNumbers(tokenized.input_ids.data);
1852 | const attentionMaskData = this.convertTensorDataToNumbers(tokenized.attention_mask.data);
1853 | const tokenTypeIdsData = tokenized.token_type_ids
1854 | ? this.convertTensorDataToNumbers(tokenized.token_type_ids.data)
1855 | : undefined;
1856 |
1857 | const workerPayload: WorkerMessagePayload = {
1858 | input_ids: inputIdsData,
1859 | attention_mask: attentionMaskData,
1860 | token_type_ids: tokenTypeIdsData,
1861 | dims: {
1862 | input_ids: tokenized.input_ids.dims,
1863 | attention_mask: tokenized.attention_mask.dims,
1864 | token_type_ids: tokenized.token_type_ids?.dims,
1865 | },
1866 | };
1867 |
1868 | const workerOutput = await this._sendMessageToWorker('infer', workerPayload);
1869 | const embedding = this._extractEmbeddingFromWorkerOutput(workerOutput, attentionMaskData);
1870 | this.embeddingCache.set(cacheKey, embedding);
1871 | this.cacheStats.embedding.size = this.embeddingCache.size;
1872 |
1873 | this.performanceStats.totalEmbeddingComputations++;
1874 | this.performanceStats.totalEmbeddingTime += performance.now() - startTime;
1875 | this.performanceStats.averageEmbeddingTime =
1876 | this.performanceStats.totalEmbeddingTime / this.performanceStats.totalEmbeddingComputations;
1877 | return embedding;
1878 | } finally {
1879 | this.runningWorkerTasks--;
1880 | this.processWorkerQueue();
1881 | }
1882 | }
1883 |
1884 | public async getEmbeddingsBatch(
1885 | texts: string[],
1886 | options: Record<string, any> = {},
1887 | ): Promise<Float32Array[]> {
1888 | if (!this.isInitialized) await this.initialize();
1889 | if (!texts || texts.length === 0) return [];
1890 |
1891 | // 如果使用offscreen模式,委托给offscreen document
1892 | if (this.useOffscreen) {
1893 | // 先检查缓存
1894 | const results: (Float32Array | undefined)[] = new Array(texts.length).fill(undefined);
1895 | const uncachedTexts: string[] = [];
1896 | const uncachedIndices: number[] = [];
1897 |
1898 | texts.forEach((text, index) => {
1899 | const cacheKey = this.getCacheKey(text, options);
1900 | const cached = this.embeddingCache.get(cacheKey);
1901 | if (cached) {
1902 | results[index] = cached;
1903 | this.cacheStats.embedding.hits++;
1904 | } else {
1905 | uncachedTexts.push(text);
1906 | uncachedIndices.push(index);
1907 | this.cacheStats.embedding.misses++;
1908 | }
1909 | });
1910 |
1911 | // 如果所有都在缓存中,直接返回
1912 | if (uncachedTexts.length === 0) {
1913 | return results as Float32Array[];
1914 | }
1915 |
1916 | // 只请求未缓存的文本
1917 | const response = await chrome.runtime.sendMessage({
1918 | target: 'offscreen',
1919 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_BATCH_COMPUTE,
1920 | texts: uncachedTexts,
1921 | options: options,
1922 | });
1923 |
1924 | if (!response || !response.success) {
1925 | throw new Error(
1926 | response?.error || 'Failed to get embeddings batch from offscreen document',
1927 | );
1928 | }
1929 |
1930 | // 将结果放回对应位置并缓存
1931 | response.embeddings.forEach((embeddingArray: number[], batchIndex: number) => {
1932 | const embedding = new Float32Array(embeddingArray);
1933 | const originalIndex = uncachedIndices[batchIndex];
1934 | const originalText = uncachedTexts[batchIndex];
1935 |
1936 | results[originalIndex] = embedding;
1937 |
1938 | // 缓存结果
1939 | const cacheKey = this.getCacheKey(originalText, options);
1940 | this.embeddingCache.set(cacheKey, embedding);
1941 | });
1942 |
1943 | this.cacheStats.embedding.size = this.embeddingCache.size;
1944 | this.performanceStats.totalEmbeddingComputations += uncachedTexts.length;
1945 |
1946 | return results as Float32Array[];
1947 | }
1948 |
1949 | const results: (Float32Array | undefined)[] = new Array(texts.length).fill(undefined);
1950 | const uncachedTextsMap = new Map<string, number[]>();
1951 | const textsToTokenize: string[] = [];
1952 |
1953 | texts.forEach((text, index) => {
1954 | const cacheKey = this.getCacheKey(text, options);
1955 | const cached = this.embeddingCache.get(cacheKey);
1956 | if (cached) {
1957 | results[index] = cached;
1958 | this.cacheStats.embedding.hits++;
1959 | } else {
1960 | if (!uncachedTextsMap.has(text)) {
1961 | uncachedTextsMap.set(text, []);
1962 | textsToTokenize.push(text);
1963 | }
1964 | uncachedTextsMap.get(text)!.push(index);
1965 | this.cacheStats.embedding.misses++;
1966 | }
1967 | });
1968 | this.cacheStats.embedding.size = this.embeddingCache.size;
1969 |
1970 | if (textsToTokenize.length === 0) return results as Float32Array[];
1971 |
1972 | if (this.runningWorkerTasks >= this.config.concurrentLimit) {
1973 | await this.waitForWorkerSlot();
1974 | }
1975 | this.runningWorkerTasks++;
1976 |
1977 | const startTime = performance.now();
1978 | try {
1979 | const tokenizedBatch = await this._tokenizeText(textsToTokenize);
1980 | const workerPayload: WorkerMessagePayload = {
1981 | input_ids: this.convertTensorDataToNumbers(tokenizedBatch.input_ids.data),
1982 | attention_mask: this.convertTensorDataToNumbers(tokenizedBatch.attention_mask.data),
1983 | token_type_ids: tokenizedBatch.token_type_ids
1984 | ? this.convertTensorDataToNumbers(tokenizedBatch.token_type_ids.data)
1985 | : undefined,
1986 | dims: {
1987 | input_ids: tokenizedBatch.input_ids.dims,
1988 | attention_mask: tokenizedBatch.attention_mask.dims,
1989 | token_type_ids: tokenizedBatch.token_type_ids?.dims,
1990 | },
1991 | };
1992 |
1993 | // 使用真正的批处理推理
1994 | const workerOutput = await this._sendMessageToWorker('batchInfer', workerPayload);
1995 | const attentionMasksForBatch: number[][] = [];
1996 | const batchSize = tokenizedBatch.input_ids.dims[0];
1997 | const seqLength = tokenizedBatch.input_ids.dims[1];
1998 | const rawAttentionMaskData = this.convertTensorDataToNumbers(
1999 | tokenizedBatch.attention_mask.data,
2000 | );
2001 |
2002 | for (let i = 0; i < batchSize; ++i) {
2003 | attentionMasksForBatch.push(rawAttentionMaskData.slice(i * seqLength, (i + 1) * seqLength));
2004 | }
2005 |
2006 | const batchEmbeddings = this._extractBatchEmbeddingsFromWorkerOutput(
2007 | workerOutput,
2008 | attentionMasksForBatch,
2009 | );
2010 | batchEmbeddings.forEach((embedding, batchIdx) => {
2011 | const originalText = textsToTokenize[batchIdx];
2012 | const cacheKey = this.getCacheKey(originalText, options);
2013 | this.embeddingCache.set(cacheKey, embedding);
2014 | const originalResultIndices = uncachedTextsMap.get(originalText)!;
2015 | originalResultIndices.forEach((idx) => {
2016 | results[idx] = embedding;
2017 | });
2018 | });
2019 | this.cacheStats.embedding.size = this.embeddingCache.size;
2020 |
2021 | this.performanceStats.totalEmbeddingComputations += textsToTokenize.length;
2022 | this.performanceStats.totalEmbeddingTime += performance.now() - startTime;
2023 | this.performanceStats.averageEmbeddingTime =
2024 | this.performanceStats.totalEmbeddingTime / this.performanceStats.totalEmbeddingComputations;
2025 | return results as Float32Array[];
2026 | } finally {
2027 | this.runningWorkerTasks--;
2028 | this.processWorkerQueue();
2029 | }
2030 | }
2031 |
2032 | public async computeSimilarity(
2033 | text1: string,
2034 | text2: string,
2035 | options: Record<string, any> = {},
2036 | ): Promise<number> {
2037 | if (!this.isInitialized) await this.initialize();
2038 | this.validateInput(text1, text2);
2039 |
2040 | const simStartTime = performance.now();
2041 | const [embedding1, embedding2] = await Promise.all([
2042 | this.getEmbedding(text1, options),
2043 | this.getEmbedding(text2, options),
2044 | ]);
2045 | const similarity = this.cosineSimilarity(embedding1, embedding2);
2046 | console.log('computeSimilarity:', similarity);
2047 | this.performanceStats.totalSimilarityComputations++;
2048 | this.performanceStats.totalSimilarityTime += performance.now() - simStartTime;
2049 | this.performanceStats.averageSimilarityTime =
2050 | this.performanceStats.totalSimilarityTime / this.performanceStats.totalSimilarityComputations;
2051 | return similarity;
2052 | }
2053 |
2054 | public async computeSimilarityBatch(
2055 | pairs: { text1: string; text2: string }[],
2056 | options: Record<string, any> = {},
2057 | ): Promise<number[]> {
2058 | if (!this.isInitialized) await this.initialize();
2059 | if (!pairs || pairs.length === 0) return [];
2060 |
2061 | // 如果使用offscreen模式,委托给offscreen document
2062 | if (this.useOffscreen) {
2063 | const response = await chrome.runtime.sendMessage({
2064 | target: 'offscreen',
2065 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_BATCH_COMPUTE,
2066 | pairs: pairs,
2067 | options: options,
2068 | });
2069 |
2070 | if (!response || !response.success) {
2071 | throw new Error(response?.error || 'Failed to compute similarities in offscreen document');
2072 | }
2073 |
2074 | return response.similarities;
2075 | }
2076 |
2077 | // 直接模式的原有逻辑
2078 | const simStartTime = performance.now();
2079 | const uniqueTextsSet = new Set<string>();
2080 | pairs.forEach((pair) => {
2081 | this.validateInput(pair.text1, pair.text2);
2082 | uniqueTextsSet.add(pair.text1);
2083 | uniqueTextsSet.add(pair.text2);
2084 | });
2085 |
2086 | const uniqueTextsArray = Array.from(uniqueTextsSet);
2087 | const embeddingsArray = await this.getEmbeddingsBatch(uniqueTextsArray, options);
2088 | const embeddingMap = new Map<string, Float32Array>();
2089 | uniqueTextsArray.forEach((text, index) => {
2090 | embeddingMap.set(text, embeddingsArray[index]);
2091 | });
2092 |
2093 | const similarities = pairs.map((pair) => {
2094 | const emb1 = embeddingMap.get(pair.text1);
2095 | const emb2 = embeddingMap.get(pair.text2);
2096 | if (!emb1 || !emb2) {
2097 | console.warn('Embeddings not found for pair:', pair);
2098 | return 0;
2099 | }
2100 | return this.cosineSimilarity(emb1, emb2);
2101 | });
2102 | this.performanceStats.totalSimilarityComputations += pairs.length;
2103 | this.performanceStats.totalSimilarityTime += performance.now() - simStartTime;
2104 | this.performanceStats.averageSimilarityTime =
2105 | this.performanceStats.totalSimilarityTime / this.performanceStats.totalSimilarityComputations;
2106 | return similarities;
2107 | }
2108 |
2109 | public async computeSimilarityMatrix(
2110 | texts1: string[],
2111 | texts2: string[],
2112 | options: Record<string, any> = {},
2113 | ): Promise<number[][]> {
2114 | if (!this.isInitialized) await this.initialize();
2115 | if (!texts1 || !texts2 || texts1.length === 0 || texts2.length === 0) return [];
2116 |
2117 | const simStartTime = performance.now();
2118 | const allTextsSet = new Set<string>([...texts1, ...texts2]);
2119 | texts1.forEach((t) => this.validateInput(t, 'valid_dummy'));
2120 | texts2.forEach((t) => this.validateInput(t, 'valid_dummy'));
2121 |
2122 | const allTextsArray = Array.from(allTextsSet);
2123 | const embeddingsArray = await this.getEmbeddingsBatch(allTextsArray, options);
2124 | const embeddingMap = new Map<string, Float32Array>();
2125 | allTextsArray.forEach((text, index) => {
2126 | embeddingMap.set(text, embeddingsArray[index]);
2127 | });
2128 |
2129 | // 使用 SIMD 优化的矩阵计算(如果可用)
2130 | if (this.useSIMD && this.simdMath) {
2131 | try {
2132 | const embeddings1 = texts1.map((text) => embeddingMap.get(text)!).filter(Boolean);
2133 | const embeddings2 = texts2.map((text) => embeddingMap.get(text)!).filter(Boolean);
2134 |
2135 | if (embeddings1.length === texts1.length && embeddings2.length === texts2.length) {
2136 | const matrix = await this.simdMath.similarityMatrix(embeddings1, embeddings2);
2137 |
2138 | this.performanceStats.totalSimilarityComputations += texts1.length * texts2.length;
2139 | this.performanceStats.totalSimilarityTime += performance.now() - simStartTime;
2140 | this.performanceStats.averageSimilarityTime =
2141 | this.performanceStats.totalSimilarityTime /
2142 | this.performanceStats.totalSimilarityComputations;
2143 |
2144 | return matrix;
2145 | }
2146 | } catch (error) {
2147 | console.warn('SIMD matrix computation failed, falling back to JavaScript:', error);
2148 | }
2149 | }
2150 |
2151 | // JavaScript 回退版本
2152 | const matrix: number[][] = [];
2153 | for (const textA of texts1) {
2154 | const row: number[] = [];
2155 | const embA = embeddingMap.get(textA);
2156 | if (!embA) {
2157 | console.warn(`Embedding not found for text1: "${textA}"`);
2158 | texts2.forEach(() => row.push(0));
2159 | matrix.push(row);
2160 | continue;
2161 | }
2162 | for (const textB of texts2) {
2163 | const embB = embeddingMap.get(textB);
2164 | if (!embB) {
2165 | console.warn(`Embedding not found for text2: "${textB}"`);
2166 | row.push(0);
2167 | continue;
2168 | }
2169 | row.push(this.cosineSimilarity(embA, embB));
2170 | }
2171 | matrix.push(row);
2172 | }
2173 | this.performanceStats.totalSimilarityComputations += texts1.length * texts2.length;
2174 | this.performanceStats.totalSimilarityTime += performance.now() - simStartTime;
2175 | this.performanceStats.averageSimilarityTime =
2176 | this.performanceStats.totalSimilarityTime / this.performanceStats.totalSimilarityComputations;
2177 | return matrix;
2178 | }
2179 |
2180 | public cosineSimilarity(vecA: Float32Array, vecB: Float32Array): number {
2181 | if (!vecA || !vecB || vecA.length !== vecB.length) {
2182 | console.warn('Cosine similarity: Invalid vectors provided.', vecA, vecB);
2183 | return 0;
2184 | }
2185 |
2186 | // 使用 SIMD 优化版本(如果可用)
2187 | if (this.useSIMD && this.simdMath) {
2188 | try {
2189 | // SIMD 版本是异步的,但为了保持接口兼容性,我们需要同步版本
2190 | // 这里我们回退到 JavaScript 版本,或者可以考虑重构为异步
2191 | return this.cosineSimilarityJS(vecA, vecB);
2192 | } catch (error) {
2193 | console.warn('SIMD cosine similarity failed, falling back to JavaScript:', error);
2194 | return this.cosineSimilarityJS(vecA, vecB);
2195 | }
2196 | }
2197 |
2198 | return this.cosineSimilarityJS(vecA, vecB);
2199 | }
2200 |
2201 | private cosineSimilarityJS(vecA: Float32Array, vecB: Float32Array): number {
2202 | let dotProduct = 0;
2203 | let normA = 0;
2204 | let normB = 0;
2205 | for (let i = 0; i < vecA.length; i++) {
2206 | dotProduct += vecA[i] * vecB[i];
2207 | normA += vecA[i] * vecA[i];
2208 | normB += vecB[i] * vecB[i];
2209 | }
2210 | const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
2211 | return magnitude === 0 ? 0 : dotProduct / magnitude;
2212 | }
2213 |
2214 | // 新增:异步 SIMD 优化的余弦相似度
2215 | public async cosineSimilaritySIMD(vecA: Float32Array, vecB: Float32Array): Promise<number> {
2216 | if (!vecA || !vecB || vecA.length !== vecB.length) {
2217 | console.warn('Cosine similarity: Invalid vectors provided.', vecA, vecB);
2218 | return 0;
2219 | }
2220 |
2221 | if (this.useSIMD && this.simdMath) {
2222 | try {
2223 | return await this.simdMath.cosineSimilarity(vecA, vecB);
2224 | } catch (error) {
2225 | console.warn('SIMD cosine similarity failed, falling back to JavaScript:', error);
2226 | }
2227 | }
2228 |
2229 | return this.cosineSimilarityJS(vecA, vecB);
2230 | }
2231 |
2232 | public normalizeVector(vector: Float32Array): Float32Array {
2233 | let norm = 0;
2234 | for (let i = 0; i < vector.length; i++) norm += vector[i] * vector[i];
2235 | norm = Math.sqrt(norm);
2236 | if (norm === 0) return vector;
2237 | const normalized = new Float32Array(vector.length);
2238 | for (let i = 0; i < vector.length; i++) normalized[i] = vector[i] / norm;
2239 | return normalized;
2240 | }
2241 |
2242 | public validateInput(text1: string, text2: string | 'valid_dummy'): void {
2243 | if (typeof text1 !== 'string' || (text2 !== 'valid_dummy' && typeof text2 !== 'string')) {
2244 | throw new Error('输入必须是字符串');
2245 | }
2246 | if (text1.trim().length === 0 || (text2 !== 'valid_dummy' && text2.trim().length === 0)) {
2247 | throw new Error('输入文本不能为空');
2248 | }
2249 | const roughCharLimit = this.config.maxLength * 5;
2250 | if (
2251 | text1.length > roughCharLimit ||
2252 | (text2 !== 'valid_dummy' && text2.length > roughCharLimit)
2253 | ) {
2254 | console.warn('输入文本可能过长,将由分词器截断。');
2255 | }
2256 | }
2257 |
2258 | private getCacheKey(text: string, _options: Record<string, any> = {}): string {
2259 | return text; // Options currently not used to vary embedding, simplify key
2260 | }
2261 |
2262 | public getPerformanceStats(): Record<string, any> {
2263 | return {
2264 | ...this.performanceStats,
2265 | cacheStats: {
2266 | ...this.cacheStats,
2267 | embedding: {
2268 | ...this.cacheStats.embedding,
2269 | hitRate:
2270 | this.cacheStats.embedding.hits + this.cacheStats.embedding.misses > 0
2271 | ? this.cacheStats.embedding.hits /
2272 | (this.cacheStats.embedding.hits + this.cacheStats.embedding.misses)
2273 | : 0,
2274 | },
2275 | tokenization: {
2276 | ...this.cacheStats.tokenization,
2277 | hitRate:
2278 | this.cacheStats.tokenization.hits + this.cacheStats.tokenization.misses > 0
2279 | ? this.cacheStats.tokenization.hits /
2280 | (this.cacheStats.tokenization.hits + this.cacheStats.tokenization.misses)
2281 | : 0,
2282 | },
2283 | },
2284 | memoryPool: this.memoryPool.getStats(),
2285 | memoryUsage: this.getMemoryUsage(),
2286 | isInitialized: this.isInitialized,
2287 | isInitializing: this.isInitializing,
2288 | config: this.config,
2289 | pendingWorkerTasks: this.workerTaskQueue.length,
2290 | runningWorkerTasks: this.runningWorkerTasks,
2291 | };
2292 | }
2293 |
2294 | private async waitForWorkerSlot(): Promise<void> {
2295 | return new Promise((resolve) => {
2296 | this.workerTaskQueue.push(resolve);
2297 | });
2298 | }
2299 |
2300 | private processWorkerQueue(): void {
2301 | if (this.workerTaskQueue.length > 0 && this.runningWorkerTasks < this.config.concurrentLimit) {
2302 | const resolve = this.workerTaskQueue.shift();
2303 | if (resolve) resolve();
2304 | }
2305 | }
2306 |
2307 | // 新增:获取 Worker 统计信息
2308 | public async getWorkerStats(): Promise<WorkerStats | null> {
2309 | if (!this.worker || !this.isInitialized) return null;
2310 |
2311 | try {
2312 | const response = await this._sendMessageToWorker('getStats');
2313 | return response as WorkerStats;
2314 | } catch (error) {
2315 | console.warn('Failed to get worker stats:', error);
2316 | return null;
2317 | }
2318 | }
2319 |
2320 | // 新增:清理 Worker 缓冲区
2321 | public async clearWorkerBuffers(): Promise<void> {
2322 | if (!this.worker || !this.isInitialized) return;
2323 |
2324 | try {
2325 | await this._sendMessageToWorker('clearBuffers');
2326 | console.log('SemanticSimilarityEngine: Worker buffers cleared.');
2327 | } catch (error) {
2328 | console.warn('Failed to clear worker buffers:', error);
2329 | }
2330 | }
2331 |
2332 | // 新增:清理所有缓存
2333 | public clearAllCaches(): void {
2334 | this.embeddingCache.clear();
2335 | this.tokenizationCache.clear();
2336 | this.cacheStats = {
2337 | embedding: { hits: 0, misses: 0, size: 0 },
2338 | tokenization: { hits: 0, misses: 0, size: 0 },
2339 | };
2340 | console.log('SemanticSimilarityEngine: All caches cleared.');
2341 | }
2342 |
2343 | // 新增:获取内存使用情况
2344 | public getMemoryUsage(): {
2345 | embeddingCacheUsage: number;
2346 | tokenizationCacheUsage: number;
2347 | totalCacheUsage: number;
2348 | } {
2349 | const embeddingStats = this.embeddingCache.getStats();
2350 | const tokenizationStats = this.tokenizationCache.getStats();
2351 |
2352 | return {
2353 | embeddingCacheUsage: embeddingStats.usage,
2354 | tokenizationCacheUsage: tokenizationStats.usage,
2355 | totalCacheUsage: (embeddingStats.usage + tokenizationStats.usage) / 2,
2356 | };
2357 | }
2358 |
2359 | public async dispose(): Promise<void> {
2360 | console.log('SemanticSimilarityEngine: Disposing...');
2361 |
2362 | // 清理 Worker 缓冲区
2363 | await this.clearWorkerBuffers();
2364 |
2365 | if (this.worker) {
2366 | this.worker.terminate();
2367 | this.worker = null;
2368 | }
2369 |
2370 | // 清理 SIMD 引擎
2371 | if (this.simdMath) {
2372 | this.simdMath.dispose();
2373 | this.simdMath = null;
2374 | }
2375 |
2376 | this.tokenizer = null;
2377 | this.embeddingCache.clear();
2378 | this.tokenizationCache.clear();
2379 | this.memoryPool.clear();
2380 | this.pendingMessages.clear();
2381 | this.workerTaskQueue = [];
2382 | this.isInitialized = false;
2383 | this.isInitializing = false;
2384 | this.initPromise = null;
2385 | this.useSIMD = false;
2386 | console.log('SemanticSimilarityEngine: Disposed.');
2387 | }
2388 | }
2389 |
```