first commit
This commit is contained in:
9
app/chrome-extension/entrypoints/offscreen/index.html
Normal file
9
app/chrome-extension/entrypoints/offscreen/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
431
app/chrome-extension/entrypoints/offscreen/main.ts
Normal file
431
app/chrome-extension/entrypoints/offscreen/main.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import { SemanticSimilarityEngine } from '@/utils/semantic-similarity-engine';
|
||||
import {
|
||||
MessageTarget,
|
||||
SendMessageType,
|
||||
OFFSCREEN_MESSAGE_TYPES,
|
||||
BACKGROUND_MESSAGE_TYPES,
|
||||
} from '@/common/message-types';
|
||||
|
||||
// Global semantic similarity engine instance
|
||||
let similarityEngine: SemanticSimilarityEngine | null = null;
|
||||
interface OffscreenMessage {
|
||||
target: MessageTarget | string;
|
||||
type: SendMessageType | string;
|
||||
}
|
||||
|
||||
interface SimilarityEngineInitMessage extends OffscreenMessage {
|
||||
type: SendMessageType.SimilarityEngineInit;
|
||||
config: any;
|
||||
}
|
||||
|
||||
interface SimilarityEngineComputeBatchMessage extends OffscreenMessage {
|
||||
type: SendMessageType.SimilarityEngineComputeBatch;
|
||||
pairs: { text1: string; text2: string }[];
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface SimilarityEngineGetEmbeddingMessage extends OffscreenMessage {
|
||||
type: 'similarityEngineCompute';
|
||||
text: string;
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface SimilarityEngineGetEmbeddingsBatchMessage extends OffscreenMessage {
|
||||
type: 'similarityEngineBatchCompute';
|
||||
texts: string[];
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface SimilarityEngineStatusMessage extends OffscreenMessage {
|
||||
type: 'similarityEngineStatus';
|
||||
}
|
||||
|
||||
type MessageResponse = {
|
||||
result?: string;
|
||||
error?: string;
|
||||
success?: boolean;
|
||||
similarities?: number[];
|
||||
embedding?: number[];
|
||||
embeddings?: number[][];
|
||||
isInitialized?: boolean;
|
||||
currentConfig?: any;
|
||||
};
|
||||
|
||||
// Listen for messages from the extension
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(
|
||||
message: OffscreenMessage,
|
||||
_sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response: MessageResponse) => void,
|
||||
) => {
|
||||
if (message.target !== MessageTarget.Offscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (message.type) {
|
||||
case SendMessageType.SimilarityEngineInit:
|
||||
case OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT: {
|
||||
const initMsg = message as SimilarityEngineInitMessage;
|
||||
console.log('Offscreen: Received similarity engine init message:', message.type);
|
||||
handleSimilarityEngineInit(initMsg.config)
|
||||
.then(() => sendResponse({ success: true }))
|
||||
.catch((error) => sendResponse({ success: false, error: error.message }));
|
||||
break;
|
||||
}
|
||||
|
||||
case SendMessageType.SimilarityEngineComputeBatch: {
|
||||
const computeMsg = message as SimilarityEngineComputeBatchMessage;
|
||||
handleComputeSimilarityBatch(computeMsg.pairs, computeMsg.options)
|
||||
.then((similarities) => sendResponse({ success: true, similarities }))
|
||||
.catch((error) => sendResponse({ success: false, error: error.message }));
|
||||
break;
|
||||
}
|
||||
|
||||
case OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_COMPUTE: {
|
||||
const embeddingMsg = message as SimilarityEngineGetEmbeddingMessage;
|
||||
handleGetEmbedding(embeddingMsg.text, embeddingMsg.options)
|
||||
.then((embedding) => {
|
||||
console.log('Offscreen: Sending embedding response:', {
|
||||
length: embedding.length,
|
||||
type: typeof embedding,
|
||||
constructor: embedding.constructor.name,
|
||||
isFloat32Array: embedding instanceof Float32Array,
|
||||
firstFewValues: Array.from(embedding.slice(0, 5)),
|
||||
});
|
||||
const embeddingArray = Array.from(embedding);
|
||||
console.log('Offscreen: Converted to array:', {
|
||||
length: embeddingArray.length,
|
||||
type: typeof embeddingArray,
|
||||
isArray: Array.isArray(embeddingArray),
|
||||
firstFewValues: embeddingArray.slice(0, 5),
|
||||
});
|
||||
sendResponse({ success: true, embedding: embeddingArray });
|
||||
})
|
||||
.catch((error) => sendResponse({ success: false, error: error.message }));
|
||||
break;
|
||||
}
|
||||
|
||||
case OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_BATCH_COMPUTE: {
|
||||
const batchMsg = message as SimilarityEngineGetEmbeddingsBatchMessage;
|
||||
handleGetEmbeddingsBatch(batchMsg.texts, batchMsg.options)
|
||||
.then((embeddings) =>
|
||||
sendResponse({
|
||||
success: true,
|
||||
embeddings: embeddings.map((emb) => Array.from(emb)),
|
||||
}),
|
||||
)
|
||||
.catch((error) => sendResponse({ success: false, error: error.message }));
|
||||
break;
|
||||
}
|
||||
|
||||
case OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_STATUS: {
|
||||
handleGetEngineStatus()
|
||||
.then((status: any) => sendResponse({ success: true, ...status }))
|
||||
.catch((error: any) => sendResponse({ success: false, error: error.message }));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
sendResponse({ error: `Unknown message type: ${message.type}` });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
sendResponse({ error: error.message });
|
||||
} else {
|
||||
sendResponse({ error: 'Unknown error occurred' });
|
||||
}
|
||||
}
|
||||
|
||||
// Return true to indicate we'll respond asynchronously
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
// Global variable to track current model state
|
||||
let currentModelConfig: any = null;
|
||||
|
||||
/**
|
||||
* Check if engine reinitialization is needed
|
||||
*/
|
||||
function needsReinitialization(newConfig: any): boolean {
|
||||
if (!similarityEngine || !currentModelConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if key configuration has changed
|
||||
const keyFields = ['modelPreset', 'modelVersion', 'modelIdentifier', 'dimension'];
|
||||
for (const field of keyFields) {
|
||||
if (newConfig[field] !== currentModelConfig[field]) {
|
||||
console.log(
|
||||
`Offscreen: ${field} changed from ${currentModelConfig[field]} to ${newConfig[field]}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress callback function type
|
||||
*/
|
||||
type ProgressCallback = (progress: { status: string; progress: number; message?: string }) => void;
|
||||
|
||||
/**
|
||||
* Initialize semantic similarity engine
|
||||
*/
|
||||
async function handleSimilarityEngineInit(config: any): Promise<void> {
|
||||
console.log('Offscreen: Initializing semantic similarity engine with config:', config);
|
||||
console.log('Offscreen: Config useLocalFiles:', config.useLocalFiles);
|
||||
console.log('Offscreen: Config modelPreset:', config.modelPreset);
|
||||
console.log('Offscreen: Config modelVersion:', config.modelVersion);
|
||||
console.log('Offscreen: Config modelDimension:', config.modelDimension);
|
||||
console.log('Offscreen: Config modelIdentifier:', config.modelIdentifier);
|
||||
|
||||
// Check if reinitialization is needed
|
||||
const needsReinit = needsReinitialization(config);
|
||||
console.log('Offscreen: Needs reinitialization:', needsReinit);
|
||||
|
||||
if (!needsReinit) {
|
||||
console.log('Offscreen: Using existing engine (no changes detected)');
|
||||
await updateModelStatus('ready', 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// If engine already exists, clean up old instance first (support model switching)
|
||||
if (similarityEngine) {
|
||||
console.log('Offscreen: Cleaning up existing engine for model switch...');
|
||||
try {
|
||||
// Properly call dispose method to clean up all resources
|
||||
await similarityEngine.dispose();
|
||||
console.log('Offscreen: Previous engine disposed successfully');
|
||||
} catch (error) {
|
||||
console.warn('Offscreen: Failed to dispose previous engine:', error);
|
||||
}
|
||||
similarityEngine = null;
|
||||
currentModelConfig = null;
|
||||
|
||||
// Clear vector data in IndexedDB to ensure data consistency
|
||||
try {
|
||||
console.log('Offscreen: Clearing IndexedDB vector data for model switch...');
|
||||
await clearVectorIndexedDB();
|
||||
console.log('Offscreen: IndexedDB vector data cleared successfully');
|
||||
} catch (error) {
|
||||
console.warn('Offscreen: Failed to clear IndexedDB vector data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Update status to initializing
|
||||
await updateModelStatus('initializing', 10);
|
||||
|
||||
// Create progress callback function
|
||||
const progressCallback: ProgressCallback = async (progress) => {
|
||||
console.log('Offscreen: Progress update:', progress);
|
||||
await updateModelStatus(progress.status, progress.progress);
|
||||
};
|
||||
|
||||
// Create engine instance and pass progress callback
|
||||
similarityEngine = new SemanticSimilarityEngine(config);
|
||||
console.log('Offscreen: Starting engine initialization with progress tracking...');
|
||||
|
||||
// Use enhanced initialization method (if progress callback is supported)
|
||||
if (typeof (similarityEngine as any).initializeWithProgress === 'function') {
|
||||
await (similarityEngine as any).initializeWithProgress(progressCallback);
|
||||
} else {
|
||||
// Fallback to standard initialization method
|
||||
console.log('Offscreen: Using standard initialization (no progress callback support)');
|
||||
await updateModelStatus('downloading', 30);
|
||||
await similarityEngine.initialize();
|
||||
await updateModelStatus('ready', 100);
|
||||
}
|
||||
|
||||
// Save current configuration
|
||||
currentModelConfig = { ...config };
|
||||
|
||||
console.log('Offscreen: Semantic similarity engine initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Offscreen: Failed to initialize semantic similarity engine:', error);
|
||||
// Update status to error
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown initialization error';
|
||||
const errorType = analyzeErrorType(errorMessage);
|
||||
await updateModelStatus('error', 0, errorMessage, errorType);
|
||||
// Clean up failed instance
|
||||
similarityEngine = null;
|
||||
currentModelConfig = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear vector data in IndexedDB
|
||||
*/
|
||||
async function clearVectorIndexedDB(): Promise<void> {
|
||||
try {
|
||||
// Clear vector search related IndexedDB databases
|
||||
const dbNames = ['VectorSearchDB', 'ContentIndexerDB', 'SemanticSimilarityDB'];
|
||||
|
||||
for (const dbName of dbNames) {
|
||||
try {
|
||||
// Try to delete database
|
||||
const deleteRequest = indexedDB.deleteDatabase(dbName);
|
||||
await new Promise<void>((resolve, _reject) => {
|
||||
deleteRequest.onsuccess = () => {
|
||||
console.log(`Offscreen: Successfully deleted database: ${dbName}`);
|
||||
resolve();
|
||||
};
|
||||
deleteRequest.onerror = () => {
|
||||
console.warn(`Offscreen: Failed to delete database: ${dbName}`, deleteRequest.error);
|
||||
resolve(); // 不阻塞其他数据库的清理
|
||||
};
|
||||
deleteRequest.onblocked = () => {
|
||||
console.warn(`Offscreen: Database deletion blocked: ${dbName}`);
|
||||
resolve(); // 不阻塞其他数据库的清理
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`Offscreen: Error deleting database ${dbName}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Offscreen: Failed to clear vector IndexedDB:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze error type
|
||||
function analyzeErrorType(errorMessage: string): 'network' | 'file' | 'unknown' {
|
||||
const message = errorMessage.toLowerCase();
|
||||
|
||||
if (
|
||||
message.includes('network') ||
|
||||
message.includes('fetch') ||
|
||||
message.includes('timeout') ||
|
||||
message.includes('connection') ||
|
||||
message.includes('cors') ||
|
||||
message.includes('failed to fetch')
|
||||
) {
|
||||
return 'network';
|
||||
}
|
||||
|
||||
if (
|
||||
message.includes('corrupt') ||
|
||||
message.includes('invalid') ||
|
||||
message.includes('format') ||
|
||||
message.includes('parse') ||
|
||||
message.includes('decode') ||
|
||||
message.includes('onnx')
|
||||
) {
|
||||
return 'file';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Helper function to update model status
|
||||
async function updateModelStatus(
|
||||
status: string,
|
||||
progress: number,
|
||||
errorMessage?: string,
|
||||
errorType?: string,
|
||||
) {
|
||||
try {
|
||||
const modelState = {
|
||||
status,
|
||||
downloadProgress: progress,
|
||||
isDownloading: status === 'downloading' || status === 'initializing',
|
||||
lastUpdated: Date.now(),
|
||||
errorMessage: errorMessage || '',
|
||||
errorType: errorType || '',
|
||||
};
|
||||
|
||||
// In offscreen document, update storage through message passing to background script
|
||||
// because offscreen document may not have direct chrome.storage access
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
|
||||
await chrome.storage.local.set({ modelState });
|
||||
} else {
|
||||
// If chrome.storage is not available, pass message to background script
|
||||
console.log('Offscreen: chrome.storage not available, sending message to background');
|
||||
try {
|
||||
await chrome.runtime.sendMessage({
|
||||
type: BACKGROUND_MESSAGE_TYPES.UPDATE_MODEL_STATUS,
|
||||
modelState: modelState,
|
||||
});
|
||||
} catch (messageError) {
|
||||
console.error('Offscreen: Failed to send status update message:', messageError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Offscreen: Failed to update model status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch compute semantic similarity
|
||||
*/
|
||||
async function handleComputeSimilarityBatch(
|
||||
pairs: { text1: string; text2: string }[],
|
||||
options: Record<string, any> = {},
|
||||
): Promise<number[]> {
|
||||
if (!similarityEngine) {
|
||||
throw new Error('Similarity engine not initialized. Please reinitialize the engine.');
|
||||
}
|
||||
|
||||
console.log(`Offscreen: Computing similarities for ${pairs.length} pairs`);
|
||||
const similarities = await similarityEngine.computeSimilarityBatch(pairs, options);
|
||||
console.log('Offscreen: Similarity computation completed');
|
||||
|
||||
return similarities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get embedding vector for single text
|
||||
*/
|
||||
async function handleGetEmbedding(
|
||||
text: string,
|
||||
options: Record<string, any> = {},
|
||||
): Promise<Float32Array> {
|
||||
if (!similarityEngine) {
|
||||
throw new Error('Similarity engine not initialized. Please reinitialize the engine.');
|
||||
}
|
||||
|
||||
console.log(`Offscreen: Getting embedding for text: "${text.substring(0, 50)}..."`);
|
||||
const embedding = await similarityEngine.getEmbedding(text, options);
|
||||
console.log('Offscreen: Embedding computation completed');
|
||||
|
||||
return embedding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch get embedding vectors for texts
|
||||
*/
|
||||
async function handleGetEmbeddingsBatch(
|
||||
texts: string[],
|
||||
options: Record<string, any> = {},
|
||||
): Promise<Float32Array[]> {
|
||||
if (!similarityEngine) {
|
||||
throw new Error('Similarity engine not initialized. Please reinitialize the engine.');
|
||||
}
|
||||
|
||||
console.log(`Offscreen: Getting embeddings for ${texts.length} texts`);
|
||||
const embeddings = await similarityEngine.getEmbeddingsBatch(texts, options);
|
||||
console.log('Offscreen: Batch embedding computation completed');
|
||||
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engine status
|
||||
*/
|
||||
async function handleGetEngineStatus(): Promise<{
|
||||
isInitialized: boolean;
|
||||
currentConfig: any;
|
||||
}> {
|
||||
return {
|
||||
isInitialized: !!similarityEngine,
|
||||
currentConfig: currentModelConfig,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Offscreen: Semantic similarity engine handler loaded');
|
Reference in New Issue
Block a user