#
tokens: 28714/50000 1/120 files (page 7/10)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 7/10FirstPrevNextLast