This is page 3 of 8. Use http://codebase.md/hangwin/mcp-chrome?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/_locales/de/messages.json:
--------------------------------------------------------------------------------
```json
{
"extensionName": {
"message": "chrome-mcp-server",
"description": "Erweiterungsname"
},
"extensionDescription": {
"message": "Stellt Browser-Funktionen mit Ihrem eigenen Chrome zur Verfügung",
"description": "Erweiterungsbeschreibung"
},
"nativeServerConfigLabel": {
"message": "Native Server-Konfiguration",
"description": "Hauptabschnittstitel für Native Server-Einstellungen"
},
"semanticEngineLabel": {
"message": "Semantische Engine",
"description": "Hauptabschnittstitel für semantische Engine"
},
"embeddingModelLabel": {
"message": "Embedding-Modell",
"description": "Hauptabschnittstitel für Modellauswahl"
},
"indexDataManagementLabel": {
"message": "Index-Datenverwaltung",
"description": "Hauptabschnittstitel für Datenverwaltung"
},
"modelCacheManagementLabel": {
"message": "Modell-Cache-Verwaltung",
"description": "Hauptabschnittstitel für Cache-Verwaltung"
},
"statusLabel": {
"message": "Status",
"description": "Allgemeines Statuslabel"
},
"runningStatusLabel": {
"message": "Betriebsstatus",
"description": "Server-Betriebsstatuslabel"
},
"connectionStatusLabel": {
"message": "Verbindungsstatus",
"description": "Verbindungsstatuslabel"
},
"lastUpdatedLabel": {
"message": "Zuletzt aktualisiert:",
"description": "Zeitstempel der letzten Aktualisierung"
},
"connectButton": {
"message": "Verbinden",
"description": "Verbinden-Schaltflächentext"
},
"disconnectButton": {
"message": "Trennen",
"description": "Trennen-Schaltflächentext"
},
"connectingStatus": {
"message": "Verbindung wird hergestellt...",
"description": "Verbindungsstatusmeldung"
},
"connectedStatus": {
"message": "Verbunden",
"description": "Verbunden-Statusmeldung"
},
"disconnectedStatus": {
"message": "Getrennt",
"description": "Getrennt-Statusmeldung"
},
"detectingStatus": {
"message": "Erkennung läuft...",
"description": "Erkennungsstatusmeldung"
},
"serviceRunningStatus": {
"message": "Service läuft (Port: $PORT$)",
"description": "Service läuft mit Portnummer",
"placeholders": {
"port": {
"content": "$1",
"example": "12306"
}
}
},
"serviceNotConnectedStatus": {
"message": "Service nicht verbunden",
"description": "Service nicht verbunden Status"
},
"connectedServiceNotStartedStatus": {
"message": "Verbunden, Service nicht gestartet",
"description": "Verbunden aber Service nicht gestartet Status"
},
"mcpServerConfigLabel": {
"message": "MCP Server-Konfiguration",
"description": "MCP Server-Konfigurationsabschnittslabel"
},
"connectionPortLabel": {
"message": "Verbindungsport",
"description": "Verbindungsport-Eingabelabel"
},
"refreshStatusButton": {
"message": "Status aktualisieren",
"description": "Status aktualisieren Schaltflächen-Tooltip"
},
"copyConfigButton": {
"message": "Konfiguration kopieren",
"description": "Konfiguration kopieren Schaltflächentext"
},
"retryButton": {
"message": "Wiederholen",
"description": "Wiederholen-Schaltflächentext"
},
"cancelButton": {
"message": "Abbrechen",
"description": "Abbrechen-Schaltflächentext"
},
"confirmButton": {
"message": "Bestätigen",
"description": "Bestätigen-Schaltflächentext"
},
"saveButton": {
"message": "Speichern",
"description": "Speichern-Schaltflächentext"
},
"closeButton": {
"message": "Schließen",
"description": "Schließen-Schaltflächentext"
},
"resetButton": {
"message": "Zurücksetzen",
"description": "Zurücksetzen-Schaltflächentext"
},
"initializingStatus": {
"message": "Initialisierung...",
"description": "Initialisierung-Fortschrittsmeldung"
},
"processingStatus": {
"message": "Verarbeitung...",
"description": "Verarbeitung-Fortschrittsmeldung"
},
"loadingStatus": {
"message": "Wird geladen...",
"description": "Ladefortschrittsmeldung"
},
"clearingStatus": {
"message": "Wird geleert...",
"description": "Leerungsfortschrittsmeldung"
},
"cleaningStatus": {
"message": "Wird bereinigt...",
"description": "Bereinigungsfortschrittsmeldung"
},
"downloadingStatus": {
"message": "Wird heruntergeladen...",
"description": "Download-Fortschrittsmeldung"
},
"semanticEngineReadyStatus": {
"message": "Semantische Engine bereit",
"description": "Semantische Engine bereit Status"
},
"semanticEngineInitializingStatus": {
"message": "Semantische Engine wird initialisiert...",
"description": "Semantische Engine Initialisierungsstatus"
},
"semanticEngineInitFailedStatus": {
"message": "Initialisierung der semantischen Engine fehlgeschlagen",
"description": "Semantische Engine Initialisierung fehlgeschlagen Status"
},
"semanticEngineNotInitStatus": {
"message": "Semantische Engine nicht initialisiert",
"description": "Semantische Engine nicht initialisiert Status"
},
"initSemanticEngineButton": {
"message": "Semantische Engine initialisieren",
"description": "Semantische Engine initialisieren Schaltflächentext"
},
"reinitializeButton": {
"message": "Neu initialisieren",
"description": "Neu initialisieren Schaltflächentext"
},
"downloadingModelStatus": {
"message": "Modell wird heruntergeladen... $PROGRESS$%",
"description": "Modell-Download-Fortschritt mit Prozentsatz",
"placeholders": {
"progress": {
"content": "$1",
"example": "50"
}
}
},
"switchingModelStatus": {
"message": "Modell wird gewechselt...",
"description": "Modellwechsel-Fortschrittsmeldung"
},
"modelLoadedStatus": {
"message": "Modell geladen",
"description": "Modell erfolgreich geladen Status"
},
"modelFailedStatus": {
"message": "Modell konnte nicht geladen werden",
"description": "Modell-Ladefehler Status"
},
"lightweightModelDescription": {
"message": "Leichtgewichtiges mehrsprachiges Modell",
"description": "Beschreibung für leichtgewichtige Modelloption"
},
"betterThanSmallDescription": {
"message": "Etwas größer als e5-small, aber bessere Leistung",
"description": "Beschreibung für mittlere Modelloption"
},
"multilingualModelDescription": {
"message": "Mehrsprachiges semantisches Modell",
"description": "Beschreibung für mehrsprachige Modelloption"
},
"fastPerformance": {
"message": "Schnell",
"description": "Schnelle Leistungsanzeige"
},
"balancedPerformance": {
"message": "Ausgewogen",
"description": "Ausgewogene Leistungsanzeige"
},
"accuratePerformance": {
"message": "Genau",
"description": "Genaue Leistungsanzeige"
},
"networkErrorMessage": {
"message": "Netzwerkverbindungsfehler, bitte Netzwerk prüfen und erneut versuchen",
"description": "Netzwerkverbindungsfehlermeldung"
},
"modelCorruptedErrorMessage": {
"message": "Modelldatei beschädigt oder unvollständig, bitte Download wiederholen",
"description": "Modell-Beschädigungsfehlermeldung"
},
"unknownErrorMessage": {
"message": "Unbekannter Fehler, bitte prüfen Sie, ob Ihr Netzwerk auf HuggingFace zugreifen kann",
"description": "Unbekannte Fehler-Rückfallmeldung"
},
"permissionDeniedErrorMessage": {
"message": "Zugriff verweigert",
"description": "Zugriff verweigert Fehlermeldung"
},
"timeoutErrorMessage": {
"message": "Zeitüberschreitung",
"description": "Zeitüberschreitungsfehlermeldung"
},
"indexedPagesLabel": {
"message": "Indizierte Seiten",
"description": "Anzahl indizierter Seiten Label"
},
"indexSizeLabel": {
"message": "Indexgröße",
"description": "Indexgröße Label"
},
"activeTabsLabel": {
"message": "Aktive Tabs",
"description": "Anzahl aktiver Tabs Label"
},
"vectorDocumentsLabel": {
"message": "Vektordokumente",
"description": "Anzahl Vektordokumente Label"
},
"cacheSizeLabel": {
"message": "Cache-Größe",
"description": "Cache-Größe Label"
},
"cacheEntriesLabel": {
"message": "Cache-Einträge",
"description": "Anzahl Cache-Einträge Label"
},
"clearAllDataButton": {
"message": "Alle Daten löschen",
"description": "Alle Daten löschen Schaltflächentext"
},
"clearAllCacheButton": {
"message": "Gesamten Cache löschen",
"description": "Gesamten Cache löschen Schaltflächentext"
},
"cleanExpiredCacheButton": {
"message": "Abgelaufenen Cache bereinigen",
"description": "Abgelaufenen Cache bereinigen Schaltflächentext"
},
"exportDataButton": {
"message": "Daten exportieren",
"description": "Daten exportieren Schaltflächentext"
},
"importDataButton": {
"message": "Daten importieren",
"description": "Daten importieren Schaltflächentext"
},
"confirmClearDataTitle": {
"message": "Datenlöschung bestätigen",
"description": "Datenlöschung bestätigen Dialogtitel"
},
"settingsTitle": {
"message": "Einstellungen",
"description": "Einstellungen Dialogtitel"
},
"aboutTitle": {
"message": "Über",
"description": "Über Dialogtitel"
},
"helpTitle": {
"message": "Hilfe",
"description": "Hilfe Dialogtitel"
},
"clearDataWarningMessage": {
"message": "Diese Aktion löscht alle indizierten Webseiteninhalte und Vektordaten, einschließlich:",
"description": "Datenlöschung Warnmeldung"
},
"clearDataList1": {
"message": "Alle Webseitentextinhaltsindizes",
"description": "Erster Punkt in Datenlöschungsliste"
},
"clearDataList2": {
"message": "Vektor-Embedding-Daten",
"description": "Zweiter Punkt in Datenlöschungsliste"
},
"clearDataList3": {
"message": "Suchverlauf und Cache",
"description": "Dritter Punkt in Datenlöschungsliste"
},
"clearDataIrreversibleWarning": {
"message": "Diese Aktion ist unwiderruflich! Nach dem Löschen müssen Sie Webseiten erneut durchsuchen, um den Index neu aufzubauen.",
"description": "Unwiderrufliche Aktion Warnung"
},
"confirmClearButton": {
"message": "Löschung bestätigen",
"description": "Löschung bestätigen Aktionsschaltfläche"
},
"cacheDetailsLabel": {
"message": "Cache-Details",
"description": "Cache-Details Abschnittslabel"
},
"noCacheDataMessage": {
"message": "Keine Cache-Daten vorhanden",
"description": "Keine Cache-Daten verfügbar Meldung"
},
"loadingCacheInfoStatus": {
"message": "Cache-Informationen werden geladen...",
"description": "Cache-Informationen laden Status"
},
"processingCacheStatus": {
"message": "Cache wird verarbeitet...",
"description": "Cache verarbeiten Status"
},
"expiredLabel": {
"message": "Abgelaufen",
"description": "Abgelaufenes Element Label"
},
"bookmarksBarLabel": {
"message": "Lesezeichenleiste",
"description": "Lesezeichenleiste Ordnername"
},
"newTabLabel": {
"message": "Neuer Tab",
"description": "Neuer Tab Label"
},
"currentPageLabel": {
"message": "Aktuelle Seite",
"description": "Aktuelle Seite Label"
},
"menuLabel": {
"message": "Menü",
"description": "Menü Barrierefreiheitslabel"
},
"navigationLabel": {
"message": "Navigation",
"description": "Navigation Barrierefreiheitslabel"
},
"mainContentLabel": {
"message": "Hauptinhalt",
"description": "Hauptinhalt Barrierefreiheitslabel"
},
"languageSelectorLabel": {
"message": "Sprache",
"description": "Sprachauswahl Label"
},
"themeLabel": {
"message": "Design",
"description": "Design-Auswahl Label"
},
"lightTheme": {
"message": "Hell",
"description": "Helles Design Option"
},
"darkTheme": {
"message": "Dunkel",
"description": "Dunkles Design Option"
},
"autoTheme": {
"message": "Automatisch",
"description": "Automatisches Design Option"
},
"advancedSettingsLabel": {
"message": "Erweiterte Einstellungen",
"description": "Erweiterte Einstellungen Abschnittslabel"
},
"debugModeLabel": {
"message": "Debug-Modus",
"description": "Debug-Modus Umschalter Label"
},
"verboseLoggingLabel": {
"message": "Ausführliche Protokollierung",
"description": "Ausführliche Protokollierung Umschalter Label"
},
"successNotification": {
"message": "Vorgang erfolgreich abgeschlossen",
"description": "Allgemeine Erfolgsmeldung"
},
"warningNotification": {
"message": "Warnung: Bitte prüfen Sie vor dem Fortfahren",
"description": "Allgemeine Warnmeldung"
},
"infoNotification": {
"message": "Information",
"description": "Allgemeine Informationsmeldung"
},
"configCopiedNotification": {
"message": "Konfiguration in Zwischenablage kopiert",
"description": "Konfiguration kopiert Erfolgsmeldung"
},
"dataClearedNotification": {
"message": "Daten erfolgreich gelöscht",
"description": "Daten gelöscht Erfolgsmeldung"
},
"bytesUnit": {
"message": "Bytes",
"description": "Bytes Einheit"
},
"kilobytesUnit": {
"message": "KB",
"description": "Kilobytes Einheit"
},
"megabytesUnit": {
"message": "MB",
"description": "Megabytes Einheit"
},
"gigabytesUnit": {
"message": "GB",
"description": "Gigabytes Einheit"
},
"itemsUnit": {
"message": "Elemente",
"description": "Elemente Zähleinheit"
},
"pagesUnit": {
"message": "Seiten",
"description": "Seiten Zähleinheit"
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/simd-math-engine.ts:
--------------------------------------------------------------------------------
```typescript
/**
* SIMD-optimized mathematical computation engine
* Uses WebAssembly + SIMD instructions to accelerate vector calculations
*/
interface SIMDMathWasm {
free(): void;
cosine_similarity(vec_a: Float32Array, vec_b: Float32Array): number;
batch_similarity(vectors: Float32Array, query: Float32Array, vector_dim: number): Float32Array;
similarity_matrix(
vectors_a: Float32Array,
vectors_b: Float32Array,
vector_dim: number,
): Float32Array;
}
interface WasmModule {
SIMDMath: new () => SIMDMathWasm;
memory: WebAssembly.Memory;
default: (module_or_path?: any) => Promise<any>;
}
export class SIMDMathEngine {
private wasmModule: WasmModule | null = null;
private simdMath: SIMDMathWasm | null = null;
private isInitialized = false;
private isInitializing = false;
private initPromise: Promise<void> | null = null;
private alignedBufferPool: Map<number, Float32Array[]> = new Map();
private maxPoolSize = 5;
async initialize(): Promise<void> {
if (this.isInitialized) return;
if (this.isInitializing && this.initPromise) return this.initPromise;
this.isInitializing = true;
this.initPromise = this._doInitialize().finally(() => {
this.isInitializing = false;
});
return this.initPromise;
}
private async _doInitialize(): Promise<void> {
try {
console.log('SIMDMathEngine: Initializing WebAssembly module...');
const wasmUrl = chrome.runtime.getURL('workers/simd_math.js');
const wasmModule = await import(wasmUrl);
const wasmInstance = await wasmModule.default();
this.wasmModule = {
SIMDMath: wasmModule.SIMDMath,
memory: wasmInstance.memory,
default: wasmModule.default,
};
this.simdMath = new this.wasmModule.SIMDMath();
this.isInitialized = true;
console.log('SIMDMathEngine: WebAssembly module initialized successfully');
} catch (error) {
console.error('SIMDMathEngine: Failed to initialize WebAssembly module:', error);
this.isInitialized = false;
throw error;
}
}
/**
* Get aligned buffer (16-byte aligned, suitable for SIMD)
*/
private getAlignedBuffer(size: number): Float32Array {
if (!this.alignedBufferPool.has(size)) {
this.alignedBufferPool.set(size, []);
}
const pool = this.alignedBufferPool.get(size)!;
if (pool.length > 0) {
return pool.pop()!;
}
// Create 16-byte aligned buffer
const buffer = new ArrayBuffer(size * 4 + 15);
const alignedOffset = (16 - (buffer.byteLength % 16)) % 16;
return new Float32Array(buffer, alignedOffset, size);
}
/**
* Release aligned buffer back to pool
*/
private releaseAlignedBuffer(buffer: Float32Array): void {
const size = buffer.length;
const pool = this.alignedBufferPool.get(size);
if (pool && pool.length < this.maxPoolSize) {
buffer.fill(0); // Clear to zero
pool.push(buffer);
}
}
/**
* Check if vector is already aligned
*/
private isAligned(array: Float32Array): boolean {
return array.byteOffset % 16 === 0;
}
/**
* Ensure vector alignment, create aligned copy if not aligned
*/
private ensureAligned(array: Float32Array): { aligned: Float32Array; needsRelease: boolean } {
if (this.isAligned(array)) {
return { aligned: array, needsRelease: false };
}
const aligned = this.getAlignedBuffer(array.length);
aligned.set(array);
return { aligned, needsRelease: true };
}
/**
* SIMD-optimized cosine similarity calculation
*/
async cosineSimilarity(vecA: Float32Array, vecB: Float32Array): Promise<number> {
if (!this.isInitialized) {
await this.initialize();
}
if (!this.simdMath) {
throw new Error('SIMD math engine not initialized');
}
// Ensure vector alignment
const { aligned: alignedA, needsRelease: releaseA } = this.ensureAligned(vecA);
const { aligned: alignedB, needsRelease: releaseB } = this.ensureAligned(vecB);
try {
const result = this.simdMath.cosine_similarity(alignedA, alignedB);
return result;
} finally {
// Release temporary buffers
if (releaseA) this.releaseAlignedBuffer(alignedA);
if (releaseB) this.releaseAlignedBuffer(alignedB);
}
}
/**
* Batch similarity calculation
*/
async batchSimilarity(vectors: Float32Array[], query: Float32Array): Promise<number[]> {
if (!this.isInitialized) {
await this.initialize();
}
if (!this.simdMath) {
throw new Error('SIMD math engine not initialized');
}
const vectorDim = query.length;
const numVectors = vectors.length;
// Pack all vectors into contiguous memory layout
const packedVectors = this.getAlignedBuffer(numVectors * vectorDim);
const { aligned: alignedQuery, needsRelease: releaseQuery } = this.ensureAligned(query);
try {
// Copy vector data
let offset = 0;
for (const vector of vectors) {
packedVectors.set(vector, offset);
offset += vectorDim;
}
// Batch calculation
const results = this.simdMath.batch_similarity(packedVectors, alignedQuery, vectorDim);
return Array.from(results);
} finally {
this.releaseAlignedBuffer(packedVectors);
if (releaseQuery) this.releaseAlignedBuffer(alignedQuery);
}
}
/**
* Similarity matrix calculation
*/
async similarityMatrix(vectorsA: Float32Array[], vectorsB: Float32Array[]): Promise<number[][]> {
if (!this.isInitialized) {
await this.initialize();
}
if (!this.simdMath || vectorsA.length === 0 || vectorsB.length === 0) {
return [];
}
const vectorDim = vectorsA[0].length;
const numA = vectorsA.length;
const numB = vectorsB.length;
// Pack vectors
const packedA = this.getAlignedBuffer(numA * vectorDim);
const packedB = this.getAlignedBuffer(numB * vectorDim);
try {
// Copy data
let offsetA = 0;
for (const vector of vectorsA) {
packedA.set(vector, offsetA);
offsetA += vectorDim;
}
let offsetB = 0;
for (const vector of vectorsB) {
packedB.set(vector, offsetB);
offsetB += vectorDim;
}
// Calculate matrix
const flatResults = this.simdMath.similarity_matrix(packedA, packedB, vectorDim);
// Convert to 2D array
const matrix: number[][] = [];
for (let i = 0; i < numA; i++) {
const row: number[] = [];
for (let j = 0; j < numB; j++) {
row.push(flatResults[i * numB + j]);
}
matrix.push(row);
}
return matrix;
} finally {
this.releaseAlignedBuffer(packedA);
this.releaseAlignedBuffer(packedB);
}
}
/**
* Check SIMD support
*/
static async checkSIMDSupport(): Promise<boolean> {
try {
console.log('SIMDMathEngine: Checking SIMD support...');
// Get browser information
const userAgent = navigator.userAgent;
const browserInfo = SIMDMathEngine.getBrowserInfo();
console.log('Browser info:', browserInfo);
console.log('User Agent:', userAgent);
// Check WebAssembly basic support
if (typeof WebAssembly !== 'object') {
console.log('WebAssembly not supported');
return false;
}
console.log('✅ WebAssembly basic support: OK');
// Check WebAssembly.validate method
if (typeof WebAssembly.validate !== 'function') {
console.log('❌ WebAssembly.validate not available');
return false;
}
console.log('✅ WebAssembly.validate: OK');
// Test basic WebAssembly module validation
const basicWasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
const basicValid = WebAssembly.validate(basicWasm);
console.log('✅ Basic WASM validation:', basicValid);
// Check WebAssembly SIMD support - using correct SIMD test module
console.log('Testing SIMD WASM module...');
// Method 1: Use standard SIMD detection bytecode
let wasmSIMDSupported = false;
try {
// This is a minimal SIMD module containing v128.const instruction
const simdWasm = new Uint8Array([
0x00,
0x61,
0x73,
0x6d, // WASM magic
0x01,
0x00,
0x00,
0x00, // version
0x01,
0x05,
0x01, // type section
0x60,
0x00,
0x01,
0x7b, // function type: () -> v128
0x03,
0x02,
0x01,
0x00, // function section
0x0a,
0x0a,
0x01, // code section
0x08,
0x00, // function body
0xfd,
0x0c, // v128.const
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x0b, // end
]);
wasmSIMDSupported = WebAssembly.validate(simdWasm);
console.log('Method 1 - Standard SIMD test result:', wasmSIMDSupported);
} catch (error) {
console.log('Method 1 failed:', error);
}
// Method 2: If method 1 fails, try simpler SIMD instruction
if (!wasmSIMDSupported) {
try {
// Test using i32x4.splat instruction
const simpleSimdWasm = new Uint8Array([
0x00,
0x61,
0x73,
0x6d, // WASM magic
0x01,
0x00,
0x00,
0x00, // version
0x01,
0x06,
0x01, // type section
0x60,
0x01,
0x7f,
0x01,
0x7b, // function type: (i32) -> v128
0x03,
0x02,
0x01,
0x00, // function section
0x0a,
0x07,
0x01, // code section
0x05,
0x00, // function body
0x20,
0x00, // local.get 0
0xfd,
0x0d, // i32x4.splat
0x0b, // end
]);
wasmSIMDSupported = WebAssembly.validate(simpleSimdWasm);
console.log('Method 2 - Simple SIMD test result:', wasmSIMDSupported);
} catch (error) {
console.log('Method 2 failed:', error);
}
}
// Method 3: If previous methods fail, try detecting specific SIMD features
if (!wasmSIMDSupported) {
try {
// Check if SIMD feature flags are supported
const featureTest = WebAssembly.validate(
new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
);
if (featureTest) {
// In Chrome, if basic WebAssembly works and version >= 91, SIMD is usually available
const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
if (chromeMatch && parseInt(chromeMatch[1]) >= 91) {
console.log('Method 3 - Chrome version check: SIMD should be available');
wasmSIMDSupported = true;
}
}
} catch (error) {
console.log('Method 3 failed:', error);
}
}
// Output final result
if (!wasmSIMDSupported) {
console.log('❌ SIMD not supported. Browser requirements:');
console.log('- Chrome 91+, Firefox 89+, Safari 16.4+, Edge 91+');
console.log('Your browser should support SIMD. Possible issues:');
console.log('1. Extension context limitations');
console.log('2. Security policies');
console.log('3. Feature flags disabled');
} else {
console.log('✅ SIMD supported!');
}
return wasmSIMDSupported;
} catch (error: any) {
console.error('SIMD support check failed:', error);
if (error instanceof Error) {
console.error('Error details:', {
name: error.name,
message: error.message,
stack: error.stack,
});
}
return false;
}
}
/**
* Get browser information
*/
static getBrowserInfo(): { name: string; version: string; supported: boolean } {
const userAgent = navigator.userAgent;
let browserName = 'Unknown';
let version = 'Unknown';
let supported = false;
// Chrome
if (userAgent.includes('Chrome/')) {
browserName = 'Chrome';
const match = userAgent.match(/Chrome\/(\d+)/);
if (match) {
version = match[1];
supported = parseInt(version) >= 91;
}
}
// Firefox
else if (userAgent.includes('Firefox/')) {
browserName = 'Firefox';
const match = userAgent.match(/Firefox\/(\d+)/);
if (match) {
version = match[1];
supported = parseInt(version) >= 89;
}
}
// Safari
else if (userAgent.includes('Safari/') && !userAgent.includes('Chrome/')) {
browserName = 'Safari';
const match = userAgent.match(/Version\/(\d+\.\d+)/);
if (match) {
version = match[1];
const versionNum = parseFloat(version);
supported = versionNum >= 16.4;
}
}
// Edge
else if (userAgent.includes('Edg/')) {
browserName = 'Edge';
const match = userAgent.match(/Edg\/(\d+)/);
if (match) {
version = match[1];
supported = parseInt(version) >= 91;
}
}
return { name: browserName, version, supported };
}
getStats() {
return {
isInitialized: this.isInitialized,
isInitializing: this.isInitializing,
bufferPoolStats: Array.from(this.alignedBufferPool.entries()).map(([size, buffers]) => ({
size,
pooled: buffers.length,
maxPoolSize: this.maxPoolSize,
})),
};
}
dispose(): void {
if (this.simdMath) {
try {
this.simdMath.free();
} catch (error) {
console.warn('Failed to free SIMD math instance:', error);
}
this.simdMath = null;
}
this.alignedBufferPool.clear();
this.wasmModule = null;
this.isInitialized = false;
this.isInitializing = false;
this.initPromise = null;
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/screenshot.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';
import {
canvasToDataURL,
createImageBitmapFromUrl,
cropAndResizeImage,
stitchImages,
compressImage,
} from '../../../../utils/image-utils';
// Screenshot-specific constants
const SCREENSHOT_CONSTANTS = {
SCROLL_DELAY_MS: 350, // Time to wait after scroll for rendering and lazy loading
CAPTURE_STITCH_DELAY_MS: 50, // Small delay between captures in a scroll sequence
MAX_CAPTURE_PARTS: 50, // Maximum number of parts to capture (for infinite scroll pages)
MAX_CAPTURE_HEIGHT_PX: 50000, // Maximum height in pixels to capture
PIXEL_TOLERANCE: 1,
SCRIPT_INIT_DELAY: 100, // Delay for script initialization
} as {
readonly SCROLL_DELAY_MS: number;
CAPTURE_STITCH_DELAY_MS: number; // This one is mutable
readonly MAX_CAPTURE_PARTS: number;
readonly MAX_CAPTURE_HEIGHT_PX: number;
readonly PIXEL_TOLERANCE: number;
readonly SCRIPT_INIT_DELAY: number;
};
SCREENSHOT_CONSTANTS["CAPTURE_STITCH_DELAY_MS"] = Math.max(1000 / chrome.tabs.MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND - SCREENSHOT_CONSTANTS.SCROLL_DELAY_MS, SCREENSHOT_CONSTANTS.CAPTURE_STITCH_DELAY_MS)
interface ScreenshotToolParams {
name: string;
selector?: string;
width?: number;
height?: number;
storeBase64?: boolean;
fullPage?: boolean;
savePng?: boolean;
maxHeight?: number; // Maximum height to capture in pixels (for infinite scroll pages)
}
/**
* Tool for capturing screenshots of web pages
*/
class ScreenshotTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.SCREENSHOT;
/**
* Execute screenshot operation
*/
async execute(args: ScreenshotToolParams): Promise<ToolResult> {
const {
name = 'screenshot',
selector,
storeBase64 = false,
fullPage = false,
savePng = true,
} = args;
console.log(`Starting screenshot with options:`, args);
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
}
const tab = tabs[0];
// Check URL restrictions
if (
tab.url?.startsWith('chrome://') ||
tab.url?.startsWith('edge://') ||
tab.url?.startsWith('https://chrome.google.com/webstore') ||
tab.url?.startsWith('https://microsoftedge.microsoft.com/')
) {
return createErrorResponse(
'Cannot capture special browser pages or web store pages due to security restrictions.',
);
}
let finalImageDataUrl: string | undefined;
const results: any = { base64: null, fileSaved: false };
let originalScroll = { x: 0, y: 0 };
try {
await this.injectContentScript(tab.id!, ['inject-scripts/screenshot-helper.js']);
// Wait for script initialization
await new Promise((resolve) => setTimeout(resolve, SCREENSHOT_CONSTANTS.SCRIPT_INIT_DELAY));
// 1. Prepare page (hide scrollbars, potentially fixed elements)
await this.sendMessageToTab(tab.id!, {
action: TOOL_MESSAGE_TYPES.SCREENSHOT_PREPARE_PAGE_FOR_CAPTURE,
options: { fullPage },
});
// Get initial page details, including original scroll position
const pageDetails = await this.sendMessageToTab(tab.id!, {
action: TOOL_MESSAGE_TYPES.SCREENSHOT_GET_PAGE_DETAILS,
});
originalScroll = { x: pageDetails.currentScrollX, y: pageDetails.currentScrollY };
if (fullPage) {
this.logInfo('Capturing full page...');
finalImageDataUrl = await this._captureFullPage(tab.id!, args, pageDetails);
} else if (selector) {
this.logInfo(`Capturing element: ${selector}`);
finalImageDataUrl = await this._captureElement(tab.id!, args, pageDetails.devicePixelRatio);
} else {
// Visible area only
this.logInfo('Capturing visible area...');
finalImageDataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
}
if (!finalImageDataUrl) {
throw new Error('Failed to capture image data');
}
// 2. Process output
if (storeBase64 === true) {
// Compress image for base64 output to reduce size
const compressed = await compressImage(finalImageDataUrl, {
scale: 0.7, // Reduce dimensions by 30%
quality: 0.8, // 80% quality for good balance
format: 'image/jpeg', // JPEG for better compression
});
// Include base64 data in response (without prefix)
const base64Data = compressed.dataUrl.replace(/^data:image\/[^;]+;base64,/, '');
results.base64 = base64Data;
return {
content: [
{
type: 'text',
text: JSON.stringify({ base64Data, mimeType: compressed.mimeType }),
},
],
isError: false,
};
}
if (savePng === true) {
// Save PNG file to downloads
this.logInfo('Saving PNG...');
try {
// Generate filename
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${name.replace(/[^a-z0-9_-]/gi, '_') || 'screenshot'}_${timestamp}.png`;
// Use Chrome's download API to save the file
const downloadId = await chrome.downloads.download({
url: finalImageDataUrl,
filename: filename,
saveAs: false,
});
results.downloadId = downloadId;
results.filename = filename;
results.fileSaved = true;
// Try to get the full file path
try {
// Wait a moment to ensure download info is updated
await new Promise((resolve) => setTimeout(resolve, 100));
// Search for download item to get full path
const [downloadItem] = await chrome.downloads.search({ id: downloadId });
if (downloadItem && downloadItem.filename) {
// Add full path to response
results.fullPath = downloadItem.filename;
}
} catch (pathError) {
console.warn('Could not get full file path:', pathError);
}
} catch (error) {
console.error('Error saving PNG file:', error);
results.saveError = String(error instanceof Error ? error.message : error);
}
}
} catch (error) {
console.error('Error during screenshot execution:', error);
return createErrorResponse(
`Screenshot error: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
);
} finally {
// 3. Reset page
try {
await this.sendMessageToTab(tab.id!, {
action: TOOL_MESSAGE_TYPES.SCREENSHOT_RESET_PAGE_AFTER_CAPTURE,
scrollX: originalScroll.x,
scrollY: originalScroll.y,
});
} catch (err) {
console.warn('Failed to reset page, tab might have closed:', err);
}
}
this.logInfo('Screenshot completed!');
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Screenshot [${name}] captured successfully`,
tabId: tab.id,
url: tab.url,
name: name,
...results,
}),
},
],
isError: false,
};
}
/**
* Log information
*/
private logInfo(message: string) {
console.log(`[Screenshot Tool] ${message}`);
}
/**
* Capture specific element
*/
async _captureElement(
tabId: number,
options: ScreenshotToolParams,
pageDpr: number,
): Promise<string> {
const elementDetails = await this.sendMessageToTab(tabId, {
action: TOOL_MESSAGE_TYPES.SCREENSHOT_GET_ELEMENT_DETAILS,
selector: options.selector,
});
const dpr = elementDetails.devicePixelRatio || pageDpr || 1;
// Element rect is viewport-relative, in CSS pixels
// captureVisibleTab captures in physical pixels
const cropRectPx = {
x: elementDetails.rect.x * dpr,
y: elementDetails.rect.y * dpr,
width: elementDetails.rect.width * dpr,
height: elementDetails.rect.height * dpr,
};
// Small delay to ensure element is fully rendered after scrollIntoView
await new Promise((resolve) => setTimeout(resolve, SCREENSHOT_CONSTANTS.SCRIPT_INIT_DELAY));
const visibleCaptureDataUrl = await chrome.tabs.captureVisibleTab({ format: 'png' });
if (!visibleCaptureDataUrl) {
throw new Error('Failed to capture visible tab for element cropping');
}
const croppedCanvas = await cropAndResizeImage(
visibleCaptureDataUrl,
cropRectPx,
dpr,
options.width, // Target output width in CSS pixels
options.height, // Target output height in CSS pixels
);
return canvasToDataURL(croppedCanvas);
}
/**
* Capture full page
*/
async _captureFullPage(
tabId: number,
options: ScreenshotToolParams,
initialPageDetails: any,
): Promise<string> {
const dpr = initialPageDetails.devicePixelRatio;
const totalWidthCss = options.width || initialPageDetails.totalWidth; // Use option width if provided
const totalHeightCss = initialPageDetails.totalHeight; // Full page always uses actual height
// Apply maximum height limit for infinite scroll pages
const maxHeightPx = options.maxHeight || SCREENSHOT_CONSTANTS.MAX_CAPTURE_HEIGHT_PX;
const limitedHeightCss = Math.min(totalHeightCss, maxHeightPx / dpr);
const totalWidthPx = totalWidthCss * dpr;
const totalHeightPx = limitedHeightCss * dpr;
// Viewport dimensions (CSS pixels) - logged for debugging
this.logInfo(
`Viewport size: ${initialPageDetails.viewportWidth}x${initialPageDetails.viewportHeight} CSS pixels`,
);
this.logInfo(
`Page dimensions: ${totalWidthCss}x${totalHeightCss} CSS pixels (limited to ${limitedHeightCss} height)`,
);
const viewportHeightCss = initialPageDetails.viewportHeight;
const capturedParts = [];
let currentScrollYCss = 0;
let capturedHeightPx = 0;
let partIndex = 0;
while (capturedHeightPx < totalHeightPx && partIndex < SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS) {
this.logInfo(
`Capturing part ${partIndex + 1}... (${Math.round((capturedHeightPx / totalHeightPx) * 100)}%)`,
);
if (currentScrollYCss > 0) {
// Don't scroll for the first part if already at top
const scrollResp = await this.sendMessageToTab(tabId, {
action: TOOL_MESSAGE_TYPES.SCREENSHOT_SCROLL_PAGE,
x: 0,
y: currentScrollYCss,
scrollDelay: SCREENSHOT_CONSTANTS.SCROLL_DELAY_MS,
});
// Update currentScrollYCss based on actual scroll achieved
currentScrollYCss = scrollResp.newScrollY;
}
// Ensure rendering after scroll
await new Promise((resolve) =>
setTimeout(resolve, SCREENSHOT_CONSTANTS.CAPTURE_STITCH_DELAY_MS),
);
const dataUrl = await chrome.tabs.captureVisibleTab({ format: 'png' });
if (!dataUrl) throw new Error('captureVisibleTab returned empty during full page capture');
const yOffsetPx = currentScrollYCss * dpr;
capturedParts.push({ dataUrl, y: yOffsetPx });
const imgForHeight = await createImageBitmapFromUrl(dataUrl); // To get actual captured height
const lastPartEffectiveHeightPx = Math.min(imgForHeight.height, totalHeightPx - yOffsetPx);
capturedHeightPx = yOffsetPx + lastPartEffectiveHeightPx;
if (capturedHeightPx >= totalHeightPx - SCREENSHOT_CONSTANTS.PIXEL_TOLERANCE) break;
currentScrollYCss += viewportHeightCss;
// Prevent overscrolling past the document height for the next scroll command
if (
currentScrollYCss > totalHeightCss - viewportHeightCss &&
currentScrollYCss < totalHeightCss
) {
currentScrollYCss = totalHeightCss - viewportHeightCss;
}
partIndex++;
}
// Check if we hit any limits
if (partIndex >= SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS) {
this.logInfo(
`Reached maximum number of capture parts (${SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS}). This may be an infinite scroll page.`,
);
}
if (totalHeightCss > limitedHeightCss) {
this.logInfo(
`Page height (${totalHeightCss}px) exceeds maximum capture height (${maxHeightPx / dpr}px). Capturing limited portion.`,
);
}
this.logInfo('Stitching image...');
const finalCanvas = await stitchImages(capturedParts, totalWidthPx, totalHeightPx);
// If user specified width but not height (or vice versa for full page), resize maintaining aspect ratio
let outputCanvas = finalCanvas;
if (options.width && !options.height) {
const targetWidthPx = options.width * dpr;
const aspectRatio = finalCanvas.height / finalCanvas.width;
const targetHeightPx = targetWidthPx * aspectRatio;
outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
const ctx = outputCanvas.getContext('2d');
if (ctx) {
ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
}
} else if (options.height && !options.width) {
const targetHeightPx = options.height * dpr;
const aspectRatio = finalCanvas.width / finalCanvas.height;
const targetWidthPx = targetHeightPx * aspectRatio;
outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
const ctx = outputCanvas.getContext('2d');
if (ctx) {
ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
}
} else if (options.width && options.height) {
// Both specified, direct resize
const targetWidthPx = options.width * dpr;
const targetHeightPx = options.height * dpr;
outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
const ctx = outputCanvas.getContext('2d');
if (ctx) {
ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
}
}
return canvasToDataURL(outputCanvas);
}
}
export const screenshotTool = new ScreenshotTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/offscreen/main.ts:
--------------------------------------------------------------------------------
```typescript
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');
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/utils.ts:
--------------------------------------------------------------------------------
```typescript
import fs from 'fs';
import path from 'path';
import os from 'os';
import { execSync } from 'child_process';
import { promisify } from 'util';
import { COMMAND_NAME, DESCRIPTION, EXTENSION_ID, HOST_NAME } from './constant';
import { BrowserType, getBrowserConfig, detectInstalledBrowsers } from './browser-config';
export const access = promisify(fs.access);
export const mkdir = promisify(fs.mkdir);
export const writeFile = promisify(fs.writeFile);
/**
* 打印彩色文本
*/
export function colorText(text: string, color: string): string {
const colors: Record<string, string> = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m',
};
return colors[color] + text + colors.reset;
}
/**
* Get user-level manifest file path
*/
export function getUserManifestPath(): string {
if (os.platform() === 'win32') {
// Windows: %APPDATA%\Google\Chrome\NativeMessagingHosts\
return path.join(
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
} else if (os.platform() === 'darwin') {
// macOS: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
return path.join(
os.homedir(),
'Library',
'Application Support',
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
} else {
// Linux: ~/.config/google-chrome/NativeMessagingHosts/
return path.join(
os.homedir(),
'.config',
'google-chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
}
}
/**
* Get system-level manifest file path
*/
export function getSystemManifestPath(): string {
if (os.platform() === 'win32') {
// Windows: %ProgramFiles%\Google\Chrome\NativeMessagingHosts\
return path.join(
process.env.ProgramFiles || 'C:\\Program Files',
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
} else if (os.platform() === 'darwin') {
// macOS: /Library/Google/Chrome/NativeMessagingHosts/
return path.join('/Library', 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
} else {
// Linux: /etc/opt/chrome/native-messaging-hosts/
return path.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${HOST_NAME}.json`);
}
}
/**
* Get native host startup script file path
*/
export async function getMainPath(): Promise<string> {
try {
const packageDistDir = path.join(__dirname, '..');
const wrapperScriptName = process.platform === 'win32' ? 'run_host.bat' : 'run_host.sh';
const absoluteWrapperPath = path.resolve(packageDistDir, wrapperScriptName);
return absoluteWrapperPath;
} catch (error) {
console.log(colorText('Cannot find global package path, using current directory', 'yellow'));
throw error;
}
}
/**
* 确保关键文件具有执行权限
*/
export async function ensureExecutionPermissions(): Promise<void> {
try {
const packageDistDir = path.join(__dirname, '..');
if (process.platform === 'win32') {
// Windows 平台处理
await ensureWindowsFilePermissions(packageDistDir);
return;
}
// Unix/Linux 平台处理
const filesToCheck = [
path.join(packageDistDir, 'index.js'),
path.join(packageDistDir, 'run_host.sh'),
path.join(packageDistDir, 'cli.js'),
];
for (const filePath of filesToCheck) {
if (fs.existsSync(filePath)) {
try {
fs.chmodSync(filePath, '755');
console.log(
colorText(`✓ Set execution permissions for ${path.basename(filePath)}`, 'green'),
);
} catch (err: any) {
console.warn(
colorText(
`⚠️ Unable to set execution permissions for ${path.basename(filePath)}: ${err.message}`,
'yellow',
),
);
}
} else {
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
}
}
} catch (error: any) {
console.warn(colorText(`⚠️ Error ensuring execution permissions: ${error.message}`, 'yellow'));
}
}
/**
* Windows 平台文件权限处理
*/
async function ensureWindowsFilePermissions(packageDistDir: string): Promise<void> {
const filesToCheck = [
path.join(packageDistDir, 'index.js'),
path.join(packageDistDir, 'run_host.bat'),
path.join(packageDistDir, 'cli.js'),
];
for (const filePath of filesToCheck) {
if (fs.existsSync(filePath)) {
try {
// 检查文件是否为只读,如果是则移除只读属性
const stats = fs.statSync(filePath);
if (!(stats.mode & parseInt('200', 8))) {
// 检查写权限
// 尝试移除只读属性
fs.chmodSync(filePath, stats.mode | parseInt('200', 8));
console.log(
colorText(`✓ Removed read-only attribute from ${path.basename(filePath)}`, 'green'),
);
}
// 验证文件可读性
fs.accessSync(filePath, fs.constants.R_OK);
console.log(
colorText(`✓ Verified file accessibility for ${path.basename(filePath)}`, 'green'),
);
} catch (err: any) {
console.warn(
colorText(
`⚠️ Unable to verify file permissions for ${path.basename(filePath)}: ${err.message}`,
'yellow',
),
);
}
} else {
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
}
}
}
/**
* Create Native Messaging host manifest content
*/
export async function createManifestContent(): Promise<any> {
const mainPath = await getMainPath();
return {
name: HOST_NAME,
description: DESCRIPTION,
path: mainPath, // Node.js可执行文件路径
type: 'stdio',
allowed_origins: [`chrome-extension://${EXTENSION_ID}/`],
};
}
/**
* 验证Windows注册表项是否存在
*/
function verifyWindowsRegistryEntry(registryKey: string, expectedPath: string): boolean {
if (os.platform() !== 'win32') {
return true; // 非Windows平台跳过验证
}
try {
const result = execSync(`reg query "${registryKey}" /ve`, { encoding: 'utf8', stdio: 'pipe' });
const lines = result.split('\n');
for (const line of lines) {
if (line.includes('REG_SZ') && line.includes(expectedPath.replace(/\\/g, '\\\\'))) {
return true;
}
}
return false;
} catch (error) {
return false;
}
}
/**
* 尝试注册用户级别的Native Messaging主机
*/
export async function tryRegisterUserLevelHost(targetBrowsers?: BrowserType[]): Promise<boolean> {
try {
console.log(colorText('Attempting to register user-level Native Messaging host...', 'blue'));
// 1. 确保执行权限
await ensureExecutionPermissions();
// 2. 确定要注册的浏览器
const browsersToRegister = targetBrowsers || detectInstalledBrowsers();
if (browsersToRegister.length === 0) {
// 如果没有检测到浏览器,默认注册Chrome和Chromium
browsersToRegister.push(BrowserType.CHROME, BrowserType.CHROMIUM);
console.log(
colorText('No browsers detected, registering for Chrome and Chromium by default', 'yellow'),
);
} else {
console.log(colorText(`Detected browsers: ${browsersToRegister.join(', ')}`, 'blue'));
}
// 3. 创建清单内容
const manifest = await createManifestContent();
let successCount = 0;
const results: { browser: string; success: boolean; error?: string }[] = [];
// 4. 为每个浏览器注册
for (const browserType of browsersToRegister) {
const config = getBrowserConfig(browserType);
console.log(colorText(`\nRegistering for ${config.displayName}...`, 'blue'));
try {
// 确保目录存在
await mkdir(path.dirname(config.userManifestPath), { recursive: true });
// 写入清单文件
await writeFile(config.userManifestPath, JSON.stringify(manifest, null, 2));
console.log(colorText(`✓ Manifest written to ${config.userManifestPath}`, 'green'));
// Windows需要额外注册表项
if (os.platform() === 'win32' && config.registryKey) {
try {
const escapedPath = config.userManifestPath.replace(/\\/g, '\\\\');
const regCommand = `reg add "${config.registryKey}" /ve /t REG_SZ /d "${escapedPath}" /f`;
execSync(regCommand, { stdio: 'pipe' });
if (verifyWindowsRegistryEntry(config.registryKey, config.userManifestPath)) {
console.log(colorText(`✓ Registry entry created for ${config.displayName}`, 'green'));
} else {
throw new Error('Registry verification failed');
}
} catch (error: any) {
throw new Error(`Registry error: ${error.message}`);
}
}
successCount++;
results.push({ browser: config.displayName, success: true });
console.log(colorText(`✓ Successfully registered ${config.displayName}`, 'green'));
} catch (error: any) {
results.push({ browser: config.displayName, success: false, error: error.message });
console.log(
colorText(`✗ Failed to register ${config.displayName}: ${error.message}`, 'red'),
);
}
}
// 5. 报告结果
console.log(colorText('\n===== Registration Summary =====', 'blue'));
for (const result of results) {
if (result.success) {
console.log(colorText(`✓ ${result.browser}: Success`, 'green'));
} else {
console.log(colorText(`✗ ${result.browser}: Failed - ${result.error}`, 'red'));
}
}
return successCount > 0;
} catch (error) {
console.log(
colorText(
`User-level registration failed: ${error instanceof Error ? error.message : String(error)}`,
'yellow',
),
);
return false;
}
}
// 导入is-admin包(仅在Windows平台使用)
let isAdmin: () => boolean = () => false;
if (process.platform === 'win32') {
try {
isAdmin = require('is-admin');
} catch (error) {
console.warn('缺少is-admin依赖,Windows平台下可能无法正确检测管理员权限');
console.warn(error);
}
}
/**
* 使用提升权限注册系统级清单
*/
export async function registerWithElevatedPermissions(): Promise<void> {
try {
console.log(colorText('Attempting to register system-level manifest...', 'blue'));
// 1. 确保执行权限
await ensureExecutionPermissions();
// 2. 准备清单内容
const manifest = await createManifestContent();
// 3. 获取系统级清单路径
const manifestPath = getSystemManifestPath();
// 4. 创建临时清单文件
const tempManifestPath = path.join(os.tmpdir(), `${HOST_NAME}.json`);
await writeFile(tempManifestPath, JSON.stringify(manifest, null, 2));
// 5. 检测是否已经有管理员权限
const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac
const hasAdminRights = process.platform === 'win32' ? isAdmin() : false; // Windows平台检测管理员权限
const hasElevatedPermissions = isRoot || hasAdminRights;
// 准备命令
const command =
os.platform() === 'win32'
? `if not exist "${path.dirname(manifestPath)}" mkdir "${path.dirname(manifestPath)}" && copy "${tempManifestPath}" "${manifestPath}"`
: `mkdir -p "${path.dirname(manifestPath)}" && cp "${tempManifestPath}" "${manifestPath}" && chmod 644 "${manifestPath}"`;
if (hasElevatedPermissions) {
// 已经有管理员权限,直接执行命令
try {
// 创建目录
if (!fs.existsSync(path.dirname(manifestPath))) {
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
}
// 复制文件
fs.copyFileSync(tempManifestPath, manifestPath);
// 设置权限(非Windows平台)
if (os.platform() !== 'win32') {
fs.chmodSync(manifestPath, '644');
}
console.log(colorText('System-level manifest registration successful!', 'green'));
} catch (error: any) {
console.error(
colorText(`System-level manifest installation failed: ${error.message}`, 'red'),
);
throw error;
}
} else {
// 没有管理员权限,打印手动操作提示
console.log(
colorText('⚠️ Administrator privileges required for system-level installation', 'yellow'),
);
console.log(
colorText(
'Please run one of the following commands with administrator privileges:',
'blue',
),
);
if (os.platform() === 'win32') {
console.log(colorText(' 1. Open Command Prompt as Administrator and run:', 'blue'));
console.log(colorText(` ${command}`, 'cyan'));
} else {
console.log(colorText(' 1. Run with sudo:', 'blue'));
console.log(colorText(` sudo ${command}`, 'cyan'));
}
console.log(
colorText(' 2. Or run the registration command with elevated privileges:', 'blue'),
);
console.log(colorText(` sudo ${COMMAND_NAME} register --system`, 'cyan'));
throw new Error('Administrator privileges required for system-level installation');
}
// 6. Windows特殊处理 - 设置系统级注册表
if (os.platform() === 'win32') {
const registryKey = `HKLM\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
// 确保路径使用正确的转义格式
const escapedPath = manifestPath.replace(/\\/g, '\\\\');
const regCommand = `reg add "${registryKey}" /ve /t REG_SZ /d "${escapedPath}" /f`;
console.log(colorText(`Creating system registry entry: ${registryKey}`, 'blue'));
console.log(colorText(`Manifest path: ${manifestPath}`, 'blue'));
if (hasElevatedPermissions) {
// 已经有管理员权限,直接执行注册表命令
try {
execSync(regCommand, { stdio: 'pipe' });
// 验证注册表项是否创建成功
if (verifyWindowsRegistryEntry(registryKey, manifestPath)) {
console.log(colorText('Windows registry entry created successfully!', 'green'));
} else {
console.log(colorText('⚠️ Registry entry created but verification failed', 'yellow'));
}
} catch (error: any) {
console.error(
colorText(`Windows registry entry creation failed: ${error.message}`, 'red'),
);
console.error(colorText(`Command: ${regCommand}`, 'red'));
throw error;
}
} else {
// 没有管理员权限,打印手动操作提示
console.log(
colorText(
'⚠️ Administrator privileges required for Windows registry modification',
'yellow',
),
);
console.log(colorText('Please run the following command as Administrator:', 'blue'));
console.log(colorText(` ${regCommand}`, 'cyan'));
console.log(colorText('Or run the registration command with elevated privileges:', 'blue'));
console.log(
colorText(
` Run Command Prompt as Administrator and execute: ${COMMAND_NAME} register --system`,
'cyan',
),
);
throw new Error('Administrator privileges required for Windows registry modification');
}
}
} catch (error: any) {
console.error(colorText(`注册失败: ${error.message}`, 'red'));
throw error;
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/common.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
// Default window dimensions
const DEFAULT_WINDOW_WIDTH = 1280;
const DEFAULT_WINDOW_HEIGHT = 720;
interface NavigateToolParams {
url?: string;
newWindow?: boolean;
width?: number;
height?: number;
refresh?: boolean;
}
/**
* Tool for navigating to URLs in browser tabs or windows
*/
class NavigateTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.NAVIGATE;
async execute(args: NavigateToolParams): Promise<ToolResult> {
const { newWindow = false, width, height, url, refresh = false } = args;
console.log(
`Attempting to ${refresh ? 'refresh current tab' : `open URL: ${url}`} with options:`,
args,
);
try {
// Handle refresh option first
if (refresh) {
console.log('Refreshing current active tab');
// Get current active tab
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!activeTab || !activeTab.id) {
return createErrorResponse('No active tab found to refresh');
}
// Reload the tab
await chrome.tabs.reload(activeTab.id);
console.log(`Refreshed tab ID: ${activeTab.id}`);
// Get updated tab information
const updatedTab = await chrome.tabs.get(activeTab.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Successfully refreshed current tab',
tabId: updatedTab.id,
windowId: updatedTab.windowId,
url: updatedTab.url,
}),
},
],
isError: false,
};
}
// Validate that url is provided when not refreshing
if (!url) {
return createErrorResponse('URL parameter is required when refresh is not true');
}
// 1. Check if URL is already open
// Get all tabs and manually compare URLs
console.log(`Checking if URL is already open: ${url}`);
// Get all tabs
const allTabs = await chrome.tabs.query({});
// Manually filter matching tabs
const tabs = allTabs.filter((tab) => {
// Normalize URLs for comparison (remove trailing slashes)
const tabUrl = tab.url?.endsWith('/') ? tab.url.slice(0, -1) : tab.url;
const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
return tabUrl === targetUrl;
});
console.log(`Found ${tabs.length} matching tabs`);
if (tabs && tabs.length > 0) {
const existingTab = tabs[0];
console.log(
`URL already open in Tab ID: ${existingTab.id}, Window ID: ${existingTab.windowId}`,
);
if (existingTab.id !== undefined) {
// Activate the tab
await chrome.tabs.update(existingTab.id, { active: true });
if (existingTab.windowId !== undefined) {
// Bring the window containing this tab to the foreground and focus it
await chrome.windows.update(existingTab.windowId, { focused: true });
}
console.log(`Activated existing Tab ID: ${existingTab.id}`);
// Get updated tab information and return it
const updatedTab = await chrome.tabs.get(existingTab.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Activated existing tab',
tabId: updatedTab.id,
windowId: updatedTab.windowId,
url: updatedTab.url,
}),
},
],
isError: false,
};
}
}
// 2. If URL is not already open, decide how to open it based on options
const openInNewWindow = newWindow || typeof width === 'number' || typeof height === 'number';
if (openInNewWindow) {
console.log('Opening URL in a new window.');
// Create new window
const newWindow = await chrome.windows.create({
url: url,
width: typeof width === 'number' ? width : DEFAULT_WINDOW_WIDTH,
height: typeof height === 'number' ? height : DEFAULT_WINDOW_HEIGHT,
focused: true,
});
if (newWindow && newWindow.id !== undefined) {
console.log(`URL opened in new Window ID: ${newWindow.id}`);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Opened URL in new window',
windowId: newWindow.id,
tabs: newWindow.tabs
? newWindow.tabs.map((tab) => ({
tabId: tab.id,
url: tab.url,
}))
: [],
}),
},
],
isError: false,
};
}
} else {
console.log('Opening URL in the last active window.');
// Try to open a new tab in the most recently active window
const lastFocusedWindow = await chrome.windows.getLastFocused({ populate: false });
if (lastFocusedWindow && lastFocusedWindow.id !== undefined) {
console.log(`Found last focused Window ID: ${lastFocusedWindow.id}`);
const newTab = await chrome.tabs.create({
url: url,
windowId: lastFocusedWindow.id,
active: true,
});
// Ensure the window also gets focus
await chrome.windows.update(lastFocusedWindow.id, { focused: true });
console.log(
`URL opened in new Tab ID: ${newTab.id} in existing Window ID: ${lastFocusedWindow.id}`,
);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Opened URL in new tab in existing window',
tabId: newTab.id,
windowId: lastFocusedWindow.id,
url: newTab.url,
}),
},
],
isError: false,
};
} else {
// In rare cases, if there's no recently active window (e.g., browser just started with no windows)
// Fall back to opening in a new window
console.warn('No last focused window found, falling back to creating a new window.');
const fallbackWindow = await chrome.windows.create({
url: url,
width: DEFAULT_WINDOW_WIDTH,
height: DEFAULT_WINDOW_HEIGHT,
focused: true,
});
if (fallbackWindow && fallbackWindow.id !== undefined) {
console.log(`URL opened in fallback new Window ID: ${fallbackWindow.id}`);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Opened URL in new window',
windowId: fallbackWindow.id,
tabs: fallbackWindow.tabs
? fallbackWindow.tabs.map((tab) => ({
tabId: tab.id,
url: tab.url,
}))
: [],
}),
},
],
isError: false,
};
}
}
}
// If all attempts fail, return a generic error
return createErrorResponse('Failed to open URL: Unknown error occurred');
} catch (error) {
if (chrome.runtime.lastError) {
console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
} else {
console.error('Error in navigate:', error);
return createErrorResponse(
`Error navigating to URL: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
}
export const navigateTool = new NavigateTool();
interface CloseTabsToolParams {
tabIds?: number[];
url?: string;
}
/**
* Tool for closing browser tabs
*/
class CloseTabsTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.CLOSE_TABS;
async execute(args: CloseTabsToolParams): Promise<ToolResult> {
const { tabIds, url } = args;
let urlPattern = url;
console.log(`Attempting to close tabs with options:`, args);
try {
// If URL is provided, close all tabs matching that URL
if (urlPattern) {
console.log(`Searching for tabs with URL: ${url}`);
if (!urlPattern.endsWith('/')) {
urlPattern += '/*';
}
const tabs = await chrome.tabs.query({ url });
if (!tabs || tabs.length === 0) {
console.log(`No tabs found with URL: ${url}`);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `No tabs found with URL: ${url}`,
closedCount: 0,
}),
},
],
isError: false,
};
}
console.log(`Found ${tabs.length} tabs with URL: ${url}`);
const tabIdsToClose = tabs
.map((tab) => tab.id)
.filter((id): id is number => id !== undefined);
if (tabIdsToClose.length === 0) {
return createErrorResponse('Found tabs but could not get their IDs');
}
await chrome.tabs.remove(tabIdsToClose);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Closed ${tabIdsToClose.length} tabs with URL: ${url}`,
closedCount: tabIdsToClose.length,
closedTabIds: tabIdsToClose,
}),
},
],
isError: false,
};
}
// If tabIds are provided, close those tabs
if (tabIds && tabIds.length > 0) {
console.log(`Closing tabs with IDs: ${tabIds.join(', ')}`);
// Verify that all tabIds exist
const existingTabs = await Promise.all(
tabIds.map(async (tabId) => {
try {
return await chrome.tabs.get(tabId);
} catch (error) {
console.warn(`Tab with ID ${tabId} not found`);
return null;
}
}),
);
const validTabIds = existingTabs
.filter((tab): tab is chrome.tabs.Tab => tab !== null)
.map((tab) => tab.id)
.filter((id): id is number => id !== undefined);
if (validTabIds.length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: 'None of the provided tab IDs exist',
closedCount: 0,
}),
},
],
isError: false,
};
}
await chrome.tabs.remove(validTabIds);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Closed ${validTabIds.length} tabs`,
closedCount: validTabIds.length,
closedTabIds: validTabIds,
invalidTabIds: tabIds.filter((id) => !validTabIds.includes(id)),
}),
},
],
isError: false,
};
}
// If no tabIds or URL provided, close the current active tab
console.log('No tabIds or URL provided, closing active tab');
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!activeTab || !activeTab.id) {
return createErrorResponse('No active tab found');
}
await chrome.tabs.remove(activeTab.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Closed active tab',
closedCount: 1,
closedTabIds: [activeTab.id],
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in CloseTabsTool.execute:', error);
return createErrorResponse(
`Error closing tabs: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const closeTabsTool = new CloseTabsTool();
interface GoBackOrForwardToolParams {
isForward?: boolean;
}
/**
* Tool for navigating back or forward in browser history
*/
class GoBackOrForwardTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.GO_BACK_OR_FORWARD;
async execute(args: GoBackOrForwardToolParams): Promise<ToolResult> {
const { isForward = false } = args;
console.log(`Attempting to navigate ${isForward ? 'forward' : 'back'} in browser history`);
try {
// Get current active tab
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!activeTab || !activeTab.id) {
return createErrorResponse('No active tab found');
}
// Navigate back or forward based on the isForward parameter
if (isForward) {
await chrome.tabs.goForward(activeTab.id);
console.log(`Navigated forward in tab ID: ${activeTab.id}`);
} else {
await chrome.tabs.goBack(activeTab.id);
console.log(`Navigated back in tab ID: ${activeTab.id}`);
}
// Get updated tab information
const updatedTab = await chrome.tabs.get(activeTab.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Successfully navigated ${isForward ? 'forward' : 'back'} in browser history`,
tabId: updatedTab.id,
windowId: updatedTab.windowId,
url: updatedTab.url,
}),
},
],
isError: false,
};
} catch (error) {
if (chrome.runtime.lastError) {
console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
} else {
console.error('Error in GoBackOrForwardTool.execute:', error);
return createErrorResponse(
`Error navigating ${isForward ? 'forward' : 'back'}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
}
}
export const goBackOrForwardTool = new GoBackOrForwardTool();
interface SwitchTabToolParams {
tabId: number;
windowId?: number;
}
/**
* Tool for switching the active tab
*/
class SwitchTabTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.SWITCH_TAB;
async execute(args: SwitchTabToolParams): Promise<ToolResult> {
const { tabId, windowId } = args;
console.log(`Attempting to switch to tab ID: ${tabId} in window ID: ${windowId}`);
try {
if (windowId !== undefined) {
await chrome.windows.update(windowId, { focused: true });
}
await chrome.tabs.update(tabId, { active: true });
const updatedTab = await chrome.tabs.get(tabId);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Successfully switched to tab ID: ${tabId}`,
tabId: updatedTab.id,
windowId: updatedTab.windowId,
url: updatedTab.url,
}),
},
],
isError: false,
};
} catch (error) {
if (chrome.runtime.lastError) {
console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
} else {
console.error('Error in SwitchTabTool.execute:', error);
return createErrorResponse(
`Error switching tab: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
}
export const switchTabTool = new SwitchTabTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/content-indexer.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Content index manager
* Responsible for automatically extracting, chunking and indexing tab content
*/
import { TextChunker } from './text-chunker';
import { VectorDatabase, getGlobalVectorDatabase } from './vector-database';
import {
SemanticSimilarityEngine,
SemanticSimilarityEngineProxy,
PREDEFINED_MODELS,
type ModelPreset,
} from './semantic-similarity-engine';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
export interface IndexingOptions {
autoIndex?: boolean;
maxChunksPerPage?: number;
skipDuplicates?: boolean;
}
export class ContentIndexer {
private textChunker: TextChunker;
private vectorDatabase!: VectorDatabase;
private semanticEngine!: SemanticSimilarityEngine | SemanticSimilarityEngineProxy;
private isInitialized = false;
private isInitializing = false;
private initPromise: Promise<void> | null = null;
private indexedPages = new Set<string>();
private readonly options: Required<IndexingOptions>;
constructor(options?: IndexingOptions) {
this.options = {
autoIndex: true,
maxChunksPerPage: 50,
skipDuplicates: true,
...options,
};
this.textChunker = new TextChunker();
}
/**
* Get current selected model configuration
*/
private async getCurrentModelConfig() {
try {
const result = await chrome.storage.local.get(['selectedModel', 'selectedVersion']);
const selectedModel = (result.selectedModel as ModelPreset) || 'multilingual-e5-small';
const selectedVersion =
(result.selectedVersion as 'full' | 'quantized' | 'compressed') || 'quantized';
const modelInfo = PREDEFINED_MODELS[selectedModel];
return {
modelPreset: selectedModel,
modelIdentifier: modelInfo.modelIdentifier,
dimension: modelInfo.dimension,
modelVersion: selectedVersion,
useLocalFiles: false,
maxLength: 256,
cacheSize: 1000,
forceOffscreen: true,
};
} catch (error) {
console.error('ContentIndexer: Failed to get current model config, using default:', error);
return {
modelPreset: 'multilingual-e5-small' as const,
modelIdentifier: 'Xenova/multilingual-e5-small',
dimension: 384,
modelVersion: 'quantized' as const,
useLocalFiles: false,
maxLength: 256,
cacheSize: 1000,
forceOffscreen: true,
};
}
}
/**
* Initialize content indexer
*/
public async initialize(): Promise<void> {
if (this.isInitialized) return;
if (this.isInitializing && this.initPromise) return this.initPromise;
this.isInitializing = true;
this.initPromise = this._doInitialize().finally(() => {
this.isInitializing = false;
});
return this.initPromise;
}
private async _doInitialize(): Promise<void> {
try {
// Get current selected model configuration
const engineConfig = await this.getCurrentModelConfig();
// Use proxy class to reuse engine instance in offscreen
this.semanticEngine = new SemanticSimilarityEngineProxy(engineConfig);
await this.semanticEngine.initialize();
this.vectorDatabase = await getGlobalVectorDatabase({
dimension: engineConfig.dimension,
efSearch: 50,
});
await this.vectorDatabase.initialize();
this.setupTabEventListeners();
this.isInitialized = true;
} catch (error) {
console.error('ContentIndexer: Initialization failed:', error);
this.isInitialized = false;
throw error;
}
}
/**
* Index content of specified tab
*/
public async indexTabContent(tabId: number): Promise<void> {
// Check if semantic engine is ready before attempting to index
if (!this.isSemanticEngineReady() && !this.isSemanticEngineInitializing()) {
console.log(
`ContentIndexer: Skipping tab ${tabId} - semantic engine not ready and not initializing`,
);
return;
}
if (!this.isInitialized) {
// Only initialize if semantic engine is already ready
if (!this.isSemanticEngineReady()) {
console.log(
`ContentIndexer: Skipping tab ${tabId} - ContentIndexer not initialized and semantic engine not ready`,
);
return;
}
await this.initialize();
}
try {
const tab = await chrome.tabs.get(tabId);
if (!tab.url || !this.shouldIndexUrl(tab.url)) {
console.log(`ContentIndexer: Skipping tab ${tabId} - URL not indexable`);
return;
}
const pageKey = `${tab.url}_${tab.title}`;
if (this.options.skipDuplicates && this.indexedPages.has(pageKey)) {
console.log(`ContentIndexer: Skipping tab ${tabId} - already indexed`);
return;
}
console.log(`ContentIndexer: Starting to index tab ${tabId}: ${tab.title}`);
const content = await this.extractTabContent(tabId);
if (!content) {
console.log(`ContentIndexer: No content extracted from tab ${tabId}`);
return;
}
const chunks = this.textChunker.chunkText(content.textContent, content.title);
console.log(`ContentIndexer: Generated ${chunks.length} chunks for tab ${tabId}`);
const chunksToIndex = chunks.slice(0, this.options.maxChunksPerPage);
if (chunks.length > this.options.maxChunksPerPage) {
console.log(
`ContentIndexer: Limited chunks from ${chunks.length} to ${this.options.maxChunksPerPage}`,
);
}
for (const chunk of chunksToIndex) {
try {
const embedding = await this.semanticEngine.getEmbedding(chunk.text);
const label = await this.vectorDatabase.addDocument(
tabId,
tab.url!,
tab.title || '',
chunk,
embedding,
);
console.log(`ContentIndexer: Indexed chunk ${chunk.index} with label ${label}`);
} catch (error) {
console.error(`ContentIndexer: Failed to index chunk ${chunk.index}:`, error);
}
}
this.indexedPages.add(pageKey);
console.log(
`ContentIndexer: Successfully indexed ${chunksToIndex.length} chunks for tab ${tabId}`,
);
} catch (error) {
console.error(`ContentIndexer: Failed to index tab ${tabId}:`, error);
}
}
/**
* Search content
*/
public async searchContent(query: string, topK: number = 10) {
// Check if semantic engine is ready before attempting to search
if (!this.isSemanticEngineReady() && !this.isSemanticEngineInitializing()) {
throw new Error(
'Semantic engine is not ready yet. Please initialize the semantic engine first.',
);
}
if (!this.isInitialized) {
// Only initialize if semantic engine is already ready
if (!this.isSemanticEngineReady()) {
throw new Error(
'ContentIndexer not initialized and semantic engine not ready. Please initialize the semantic engine first.',
);
}
await this.initialize();
}
try {
const queryEmbedding = await this.semanticEngine.getEmbedding(query);
const results = await this.vectorDatabase.search(queryEmbedding, topK);
console.log(`ContentIndexer: Found ${results.length} results for query: "${query}"`);
return results;
} catch (error) {
console.error('ContentIndexer: Search failed:', error);
if (error instanceof Error && error.message.includes('not initialized')) {
console.log(
'ContentIndexer: Attempting to reinitialize semantic engine and retry search...',
);
try {
await this.semanticEngine.initialize();
const queryEmbedding = await this.semanticEngine.getEmbedding(query);
const results = await this.vectorDatabase.search(queryEmbedding, topK);
console.log(
`ContentIndexer: Retry successful, found ${results.length} results for query: "${query}"`,
);
return results;
} catch (retryError) {
console.error('ContentIndexer: Retry after reinitialization also failed:', retryError);
throw retryError;
}
}
throw error;
}
}
/**
* Remove tab index
*/
public async removeTabIndex(tabId: number): Promise<void> {
if (!this.isInitialized) {
return;
}
try {
await this.vectorDatabase.removeTabDocuments(tabId);
for (const pageKey of this.indexedPages) {
if (pageKey.includes(`tab_${tabId}_`)) {
this.indexedPages.delete(pageKey);
}
}
console.log(`ContentIndexer: Removed index for tab ${tabId}`);
} catch (error) {
console.error(`ContentIndexer: Failed to remove index for tab ${tabId}:`, error);
}
}
/**
* Check if semantic engine is ready (checks both local and global state)
*/
public isSemanticEngineReady(): boolean {
return this.semanticEngine && this.semanticEngine.isInitialized;
}
/**
* Check if global semantic engine is ready (in background/offscreen)
*/
public async isGlobalSemanticEngineReady(): Promise<boolean> {
try {
// Since ContentIndexer runs in background script, directly call the function instead of sending message
const { handleGetModelStatus } = await import('@/entrypoints/background/semantic-similarity');
const response = await handleGetModelStatus();
return (
response &&
response.success &&
response.status &&
response.status.initializationStatus === 'ready'
);
} catch (error) {
console.error('ContentIndexer: Failed to check global semantic engine status:', error);
return false;
}
}
/**
* Check if semantic engine is initializing
*/
public isSemanticEngineInitializing(): boolean {
return (
this.isInitializing || (this.semanticEngine && (this.semanticEngine as any).isInitializing)
);
}
/**
* Reinitialize content indexer (for model switching)
*/
public async reinitialize(): Promise<void> {
console.log('ContentIndexer: Reinitializing for model switch...');
this.isInitialized = false;
this.isInitializing = false;
this.initPromise = null;
await this.performCompleteDataCleanupForModelSwitch();
this.indexedPages.clear();
console.log('ContentIndexer: Cleared indexed pages cache');
try {
console.log('ContentIndexer: Creating new semantic engine proxy...');
const newEngineConfig = await this.getCurrentModelConfig();
console.log('ContentIndexer: New engine config:', newEngineConfig);
this.semanticEngine = new SemanticSimilarityEngineProxy(newEngineConfig);
console.log('ContentIndexer: New semantic engine proxy created');
await this.semanticEngine.initialize();
console.log('ContentIndexer: Semantic engine proxy initialization completed');
} catch (error) {
console.error('ContentIndexer: Failed to create new semantic engine proxy:', error);
throw error;
}
console.log(
'ContentIndexer: New semantic engine proxy is ready, proceeding with initialization',
);
await this.initialize();
console.log('ContentIndexer: Reinitialization completed successfully');
}
/**
* Perform complete data cleanup for model switching
*/
private async performCompleteDataCleanupForModelSwitch(): Promise<void> {
console.log('ContentIndexer: Starting complete data cleanup for model switch...');
try {
// Clear existing vector database instance
if (this.vectorDatabase) {
try {
console.log('ContentIndexer: Clearing existing vector database instance...');
await this.vectorDatabase.clear();
console.log('ContentIndexer: Vector database instance cleared successfully');
} catch (error) {
console.warn('ContentIndexer: Failed to clear vector database instance:', error);
}
}
try {
const { clearAllVectorData } = await import('./vector-database');
await clearAllVectorData();
console.log('ContentIndexer: Cleared all vector data for model switch');
} catch (error) {
console.warn('ContentIndexer: Failed to clear vector data:', error);
}
try {
const keysToRemove = [
'hnswlib_document_mappings_tab_content_index.dat',
'hnswlib_document_mappings_content_index.dat',
'hnswlib_document_mappings_vector_index.dat',
'vectorDatabaseStats',
'lastCleanupTime',
];
await chrome.storage.local.remove(keysToRemove);
console.log('ContentIndexer: Cleared chrome.storage model-related data');
} catch (error) {
console.warn('ContentIndexer: Failed to clear chrome.storage data:', error);
}
try {
const deleteVectorDB = indexedDB.deleteDatabase('VectorDatabaseStorage');
await new Promise<void>((resolve) => {
deleteVectorDB.onsuccess = () => {
console.log('ContentIndexer: VectorDatabaseStorage database deleted');
resolve();
};
deleteVectorDB.onerror = () => {
console.warn('ContentIndexer: Failed to delete VectorDatabaseStorage database');
resolve(); // Don't block the process
};
deleteVectorDB.onblocked = () => {
console.warn('ContentIndexer: VectorDatabaseStorage database deletion blocked');
resolve(); // Don't block the process
};
});
// Clean up hnswlib-index database
const deleteHnswDB = indexedDB.deleteDatabase('/hnswlib-index');
await new Promise<void>((resolve) => {
deleteHnswDB.onsuccess = () => {
console.log('ContentIndexer: /hnswlib-index database deleted');
resolve();
};
deleteHnswDB.onerror = () => {
console.warn('ContentIndexer: Failed to delete /hnswlib-index database');
resolve(); // Don't block the process
};
deleteHnswDB.onblocked = () => {
console.warn('ContentIndexer: /hnswlib-index database deletion blocked');
resolve(); // Don't block the process
};
});
console.log('ContentIndexer: All IndexedDB databases cleared for model switch');
} catch (error) {
console.warn('ContentIndexer: Failed to clear IndexedDB databases:', error);
}
console.log('ContentIndexer: Complete data cleanup for model switch finished successfully');
} catch (error) {
console.error('ContentIndexer: Complete data cleanup for model switch failed:', error);
throw error;
}
}
/**
* Manually trigger semantic engine initialization (async, don't wait for completion)
* Note: This should only be called after the semantic engine is already initialized
*/
public startSemanticEngineInitialization(): void {
if (!this.isInitialized && !this.isInitializing) {
console.log('ContentIndexer: Checking if semantic engine is ready...');
// Check if global semantic engine is ready before initializing ContentIndexer
this.isGlobalSemanticEngineReady()
.then((isReady) => {
if (isReady) {
console.log('ContentIndexer: Starting initialization (semantic engine ready)...');
this.initialize().catch((error) => {
console.error('ContentIndexer: Background initialization failed:', error);
});
} else {
console.log('ContentIndexer: Semantic engine not ready, skipping initialization');
}
})
.catch((error) => {
console.error('ContentIndexer: Failed to check semantic engine status:', error);
});
}
}
/**
* Get indexing statistics
*/
public getStats() {
const vectorStats = this.vectorDatabase
? this.vectorDatabase.getStats()
: {
totalDocuments: 0,
totalTabs: 0,
indexSize: 0,
};
return {
...vectorStats,
indexedPages: this.indexedPages.size,
isInitialized: this.isInitialized,
semanticEngineReady: this.isSemanticEngineReady(),
semanticEngineInitializing: this.isSemanticEngineInitializing(),
};
}
/**
* Clear all indexes
*/
public async clearAllIndexes(): Promise<void> {
if (!this.isInitialized) {
return;
}
try {
await this.vectorDatabase.clear();
this.indexedPages.clear();
console.log('ContentIndexer: All indexes cleared');
} catch (error) {
console.error('ContentIndexer: Failed to clear indexes:', error);
}
}
private setupTabEventListeners(): void {
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (this.options.autoIndex && changeInfo.status === 'complete' && tab.url) {
setTimeout(() => {
if (!this.isSemanticEngineReady() && !this.isSemanticEngineInitializing()) {
console.log(
`ContentIndexer: Skipping auto-index for tab ${tabId} - semantic engine not ready`,
);
return;
}
this.indexTabContent(tabId).catch((error) => {
console.error(`ContentIndexer: Auto-indexing failed for tab ${tabId}:`, error);
});
}, 2000);
}
});
chrome.tabs.onRemoved.addListener(async (tabId) => {
await this.removeTabIndex(tabId);
});
if (chrome.webNavigation) {
chrome.webNavigation.onCommitted.addListener(async (details) => {
if (details.frameId === 0) {
await this.removeTabIndex(details.tabId);
}
});
}
}
private shouldIndexUrl(url: string): boolean {
const excludePatterns = [
/^chrome:\/\//,
/^chrome-extension:\/\//,
/^edge:\/\//,
/^about:/,
/^moz-extension:\/\//,
/^file:\/\//,
];
return !excludePatterns.some((pattern) => pattern.test(url));
}
private async extractTabContent(
tabId: number,
): Promise<{ textContent: string; title: string } | null> {
try {
await chrome.scripting.executeScript({
target: { tabId },
files: ['inject-scripts/web-fetcher-helper.js'],
});
const response = await chrome.tabs.sendMessage(tabId, {
action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_TEXT_CONTENT,
});
if (response.success && response.textContent) {
return {
textContent: response.textContent,
title: response.title || '',
};
} else {
console.error(
`ContentIndexer: Failed to extract content from tab ${tabId}:`,
response.error,
);
return null;
}
} catch (error) {
console.error(`ContentIndexer: Error extracting content from tab ${tabId}:`, error);
return null;
}
}
}
let globalContentIndexer: ContentIndexer | null = null;
/**
* Get global ContentIndexer instance
*/
export function getGlobalContentIndexer(): ContentIndexer {
if (!globalContentIndexer) {
globalContentIndexer = new ContentIndexer();
}
return globalContentIndexer;
}
```
--------------------------------------------------------------------------------
/packages/shared/src/tools.ts:
--------------------------------------------------------------------------------
```typescript
import { type Tool } from '@modelcontextprotocol/sdk/types.js';
export const TOOL_NAMES = {
BROWSER: {
GET_WINDOWS_AND_TABS: 'get_windows_and_tabs',
SEARCH_TABS_CONTENT: 'search_tabs_content',
NAVIGATE: 'chrome_navigate',
SCREENSHOT: 'chrome_screenshot',
CLOSE_TABS: 'chrome_close_tabs',
SWITCH_TAB: 'chrome_switch_tab',
GO_BACK_OR_FORWARD: 'chrome_go_back_or_forward',
WEB_FETCHER: 'chrome_get_web_content',
CLICK: 'chrome_click_element',
FILL: 'chrome_fill_or_select',
GET_INTERACTIVE_ELEMENTS: 'chrome_get_interactive_elements',
NETWORK_CAPTURE_START: 'chrome_network_capture_start',
NETWORK_CAPTURE_STOP: 'chrome_network_capture_stop',
NETWORK_REQUEST: 'chrome_network_request',
NETWORK_DEBUGGER_START: 'chrome_network_debugger_start',
NETWORK_DEBUGGER_STOP: 'chrome_network_debugger_stop',
KEYBOARD: 'chrome_keyboard',
HISTORY: 'chrome_history',
BOOKMARK_SEARCH: 'chrome_bookmark_search',
BOOKMARK_ADD: 'chrome_bookmark_add',
BOOKMARK_DELETE: 'chrome_bookmark_delete',
INJECT_SCRIPT: 'chrome_inject_script',
SEND_COMMAND_TO_INJECT_SCRIPT: 'chrome_send_command_to_inject_script',
CONSOLE: 'chrome_console',
FILE_UPLOAD: 'chrome_upload_file',
},
};
export const TOOL_SCHEMAS: Tool[] = [
{
name: TOOL_NAMES.BROWSER.GET_WINDOWS_AND_TABS,
description: 'Get all currently open browser windows and tabs',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.NAVIGATE,
description: 'Navigate to a URL or refresh the current tab',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to navigate to the website specified' },
newWindow: {
type: 'boolean',
description: 'Create a new window to navigate to the URL or not. Defaults to false',
},
width: { type: 'number', description: 'Viewport width in pixels (default: 1280)' },
height: { type: 'number', description: 'Viewport height in pixels (default: 720)' },
refresh: {
type: 'boolean',
description:
'Refresh the current active tab instead of navigating to a URL. When true, the url parameter is ignored. Defaults to false',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.SCREENSHOT,
description:
'Take a screenshot of the current page or a specific element(if you want to see the page, recommend to use chrome_get_web_content first)',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name for the screenshot, if saving as PNG' },
selector: { type: 'string', description: 'CSS selector for element to screenshot' },
width: { type: 'number', description: 'Width in pixels (default: 800)' },
height: { type: 'number', description: 'Height in pixels (default: 600)' },
storeBase64: {
type: 'boolean',
description:
'return screenshot in base64 format (default: false) if you want to see the page, recommend set this to be true',
},
fullPage: {
type: 'boolean',
description: 'Store screenshot of the entire page (default: true)',
},
savePng: {
type: 'boolean',
description:
'Save screenshot as PNG file (default: true),if you want to see the page, recommend set this to be false, and set storeBase64 to be true',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.CLOSE_TABS,
description: 'Close one or more browser tabs',
inputSchema: {
type: 'object',
properties: {
tabIds: {
type: 'array',
items: { type: 'number' },
description: 'Array of tab IDs to close. If not provided, will close the active tab.',
},
url: {
type: 'string',
description: 'Close tabs matching this URL. Can be used instead of tabIds.',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.SWITCH_TAB,
description: 'Switch to a specific browser tab',
inputSchema: {
type: 'object',
properties: {
tabId: {
type: 'number',
description: 'The ID of the tab to switch to.',
},
windowId: {
type: 'number',
description: 'The ID of the window where the tab is located.',
},
},
required: ['tabId'],
},
},
{
name: TOOL_NAMES.BROWSER.GO_BACK_OR_FORWARD,
description: 'Navigate back or forward in browser history',
inputSchema: {
type: 'object',
properties: {
isForward: {
type: 'boolean',
description: 'Go forward in history if true, go back if false (default: false)',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.WEB_FETCHER,
description: 'Fetch content from a web page',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to fetch content from. If not provided, uses the current active tab',
},
htmlContent: {
type: 'boolean',
description:
'Get the visible HTML content of the page. If true, textContent will be ignored (default: false)',
},
textContent: {
type: 'boolean',
description:
'Get the visible text content of the page with metadata. Ignored if htmlContent is true (default: true)',
},
selector: {
type: 'string',
description:
'CSS selector to get content from a specific element. If provided, only content from this element will be returned',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.CLICK,
description: 'Click on an element in the current page or at specific coordinates',
inputSchema: {
type: 'object',
properties: {
selector: {
type: 'string',
description:
'CSS selector for the element to click. Either selector or coordinates must be provided. if coordinates are not provided, the selector must be provided.',
},
coordinates: {
type: 'object',
description:
'Coordinates to click at (relative to viewport). If provided, takes precedence over selector.',
properties: {
x: {
type: 'number',
description: 'X coordinate relative to the viewport',
},
y: {
type: 'number',
description: 'Y coordinate relative to the viewport',
},
},
required: ['x', 'y'],
},
waitForNavigation: {
type: 'boolean',
description: 'Wait for page navigation to complete after click (default: false)',
},
timeout: {
type: 'number',
description:
'Timeout in milliseconds for waiting for the element or navigation (default: 5000)',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.FILL,
description: 'Fill a form element or select an option with the specified value',
inputSchema: {
type: 'object',
properties: {
selector: {
type: 'string',
description: 'CSS selector for the input element to fill or select',
},
value: {
type: 'string',
description: 'Value to fill or select into the element',
},
},
required: ['selector', 'value'],
},
},
{
name: TOOL_NAMES.BROWSER.GET_INTERACTIVE_ELEMENTS,
description: 'Get interactive elements from the current page',
inputSchema: {
type: 'object',
properties: {
textQuery: {
type: 'string',
description: 'Text to search for within interactive elements (fuzzy search)',
},
selector: {
type: 'string',
description:
'CSS selector to filter interactive elements. Takes precedence over textQuery if both are provided.',
},
includeCoordinates: {
type: 'boolean',
description: 'Include element coordinates in the response (default: true)',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.NETWORK_REQUEST,
description: 'Send a network request from the browser with cookies and other browser context',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to send the request to',
},
method: {
type: 'string',
description: 'HTTP method to use (default: GET)',
},
headers: {
type: 'object',
description: 'Headers to include in the request',
},
body: {
type: 'string',
description: 'Body of the request (for POST, PUT, etc.)',
},
timeout: {
type: 'number',
description: 'Timeout in milliseconds (default: 30000)',
},
},
required: ['url'],
},
},
{
name: TOOL_NAMES.BROWSER.NETWORK_DEBUGGER_START,
description:
'Start capturing network requests from a web page using Chrome Debugger API(with responseBody)',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description:
'URL to capture network requests from. If not provided, uses the current active tab',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.NETWORK_DEBUGGER_STOP,
description:
'Stop capturing network requests using Chrome Debugger API and return the captured data',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.NETWORK_CAPTURE_START,
description:
'Start capturing network requests from a web page using Chrome webRequest API(without responseBody)',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description:
'URL to capture network requests from. If not provided, uses the current active tab',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.NETWORK_CAPTURE_STOP,
description:
'Stop capturing network requests using webRequest API and return the captured data',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.KEYBOARD,
description: 'Simulate keyboard events in the browser',
inputSchema: {
type: 'object',
properties: {
keys: {
type: 'string',
description: 'Keys to simulate (e.g., "Enter", "Ctrl+C", "A,B,C" for sequence)',
},
selector: {
type: 'string',
description:
'CSS selector for the element to send keyboard events to (optional, defaults to active element)',
},
delay: {
type: 'number',
description: 'Delay between key sequences in milliseconds (optional, default: 0)',
},
},
required: ['keys'],
},
},
{
name: TOOL_NAMES.BROWSER.HISTORY,
description: 'Retrieve and search browsing history from Chrome',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description:
'Text to search for in history URLs and titles. Leave empty to retrieve all history entries within the time range.',
},
startTime: {
type: 'string',
description:
'Start time as a date string. Supports ISO format (e.g., "2023-10-01", "2023-10-01T14:30:00"), relative times (e.g., "1 day ago", "2 weeks ago", "3 months ago", "1 year ago"), and special keywords ("now", "today", "yesterday"). Default: 24 hours ago',
},
endTime: {
type: 'string',
description:
'End time as a date string. Supports ISO format (e.g., "2023-10-31", "2023-10-31T14:30:00"), relative times (e.g., "1 day ago", "2 weeks ago", "3 months ago", "1 year ago"), and special keywords ("now", "today", "yesterday"). Default: current time',
},
maxResults: {
type: 'number',
description:
'Maximum number of history entries to return. Use this to limit results for performance or to focus on the most relevant entries. (default: 100)',
},
excludeCurrentTabs: {
type: 'boolean',
description:
"When set to true, filters out URLs that are currently open in any browser tab. Useful for finding pages you've visited but don't have open anymore. (default: false)",
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.BOOKMARK_SEARCH,
description: 'Search Chrome bookmarks by title and URL',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description:
'Search query to match against bookmark titles and URLs. Leave empty to retrieve all bookmarks.',
},
maxResults: {
type: 'number',
description: 'Maximum number of bookmarks to return (default: 50)',
},
folderPath: {
type: 'string',
description:
'Optional folder path or ID to limit search to a specific bookmark folder. Can be a path string (e.g., "Work/Projects") or a folder ID.',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.BOOKMARK_ADD,
description: 'Add a new bookmark to Chrome',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to bookmark. If not provided, uses the current active tab URL.',
},
title: {
type: 'string',
description: 'Title for the bookmark. If not provided, uses the page title from the URL.',
},
parentId: {
type: 'string',
description:
'Parent folder path or ID to add the bookmark to. Can be a path string (e.g., "Work/Projects") or a folder ID. If not provided, adds to the "Bookmarks Bar" folder.',
},
createFolder: {
type: 'boolean',
description: 'Whether to create the parent folder if it does not exist (default: false)',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.BOOKMARK_DELETE,
description: 'Delete a bookmark from Chrome',
inputSchema: {
type: 'object',
properties: {
bookmarkId: {
type: 'string',
description: 'ID of the bookmark to delete. Either bookmarkId or url must be provided.',
},
url: {
type: 'string',
description: 'URL of the bookmark to delete. Used if bookmarkId is not provided.',
},
title: {
type: 'string',
description: 'Title of the bookmark to help with matching when deleting by URL.',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.SEARCH_TABS_CONTENT,
description:
'search for related content from the currently open tab and return the corresponding web pages.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'the query to search for related content.',
},
},
required: ['query'],
},
},
{
name: TOOL_NAMES.BROWSER.INJECT_SCRIPT,
description:
'inject the user-specified content script into the webpage. By default, inject into the currently active tab',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description:
'If a URL is specified, inject the script into the webpage corresponding to the URL.',
},
type: {
type: 'string',
description:
'the javaScript world for a script to execute within. must be ISOLATED or MAIN',
},
jsScript: {
type: 'string',
description: 'the content script to inject',
},
},
required: ['type', 'jsScript'],
},
},
{
name: TOOL_NAMES.BROWSER.SEND_COMMAND_TO_INJECT_SCRIPT,
description:
'if the script injected using chrome_inject_script listens for user-defined events, this tool can be used to trigger those events',
inputSchema: {
type: 'object',
properties: {
tabId: {
type: 'number',
description:
'the tab where you previously injected the script(if not provided, use the currently active tab)',
},
eventName: {
type: 'string',
description: 'the eventName your injected content script listen for',
},
payload: {
type: 'string',
description: 'the payload passed to event, must be a json string',
},
},
required: ['eventName'],
},
},
{
name: TOOL_NAMES.BROWSER.CONSOLE,
description:
'Capture and retrieve all console output from the current active browser tab/page. This captures console messages that existed before the tool was called.',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description:
'URL to navigate to and capture console from. If not provided, uses the current active tab',
},
includeExceptions: {
type: 'boolean',
description: 'Include uncaught exceptions in the output (default: true)',
},
maxMessages: {
type: 'number',
description: 'Maximum number of console messages to capture (default: 100)',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.FILE_UPLOAD,
description: 'Upload files to web forms with file input elements using Chrome DevTools Protocol',
inputSchema: {
type: 'object',
properties: {
selector: {
type: 'string',
description: 'CSS selector for the file input element (input[type="file"])',
},
filePath: {
type: 'string',
description: 'Local file path to upload',
},
fileUrl: {
type: 'string',
description: 'URL to download file from before uploading',
},
base64Data: {
type: 'string',
description: 'Base64 encoded file data to upload',
},
fileName: {
type: 'string',
description: 'Optional filename when using base64 or URL (default: "uploaded-file")',
},
multiple: {
type: 'boolean',
description: 'Whether the input accepts multiple files (default: false)',
},
},
required: ['selector'],
},
},
];
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/bookmark.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { getMessage } from '@/utils/i18n';
/**
* Bookmark search tool parameters interface
*/
interface BookmarkSearchToolParams {
query?: string; // Search keywords for matching bookmark titles and URLs
maxResults?: number; // Maximum number of results to return
folderPath?: string; // Optional, specify which folder to search in (can be ID or path string like "Work/Projects")
}
/**
* Bookmark add tool parameters interface
*/
interface BookmarkAddToolParams {
url?: string; // URL to add as bookmark, if not provided use current active tab URL
title?: string; // Bookmark title, if not provided use page title
parentId?: string; // Parent folder ID or path string (like "Work/Projects"), if not provided add to "Bookmarks Bar" folder
createFolder?: boolean; // Whether to automatically create parent folder if it doesn't exist
}
/**
* Bookmark delete tool parameters interface
*/
interface BookmarkDeleteToolParams {
bookmarkId?: string; // ID of bookmark to delete
url?: string; // URL of bookmark to delete (if ID not provided, search by URL)
title?: string; // Title of bookmark to delete (used for auxiliary matching, used together with URL)
}
// --- Helper Functions ---
/**
* Get the complete folder path of a bookmark
* @param bookmarkNodeId ID of the bookmark or folder
* @returns Returns folder path string (e.g., "Bookmarks Bar > Folder A > Subfolder B")
*/
async function getBookmarkFolderPath(bookmarkNodeId: string): Promise<string> {
const pathParts: string[] = [];
try {
// First get the node itself to check if it's a bookmark or folder
const initialNodes = await chrome.bookmarks.get(bookmarkNodeId);
if (initialNodes.length > 0 && initialNodes[0]) {
const initialNode = initialNodes[0];
// Build path starting from parent node (same for both bookmarks and folders)
let pathNodeId = initialNode.parentId;
while (pathNodeId) {
const parentNodes = await chrome.bookmarks.get(pathNodeId);
if (parentNodes.length === 0) break;
const parentNode = parentNodes[0];
if (parentNode.title) {
pathParts.unshift(parentNode.title);
}
if (!parentNode.parentId) break;
pathNodeId = parentNode.parentId;
}
}
} catch (error) {
console.error(`Error getting bookmark path for node ID ${bookmarkNodeId}:`, error);
return pathParts.join(' > ') || 'Error getting path';
}
return pathParts.join(' > ');
}
/**
* Find bookmark folder by ID or path string
* If it's an ID, validate it
* If it's a path string, try to parse it
* @param pathOrId Path string (e.g., "Work/Projects") or folder ID
* @returns Returns folder node, or null if not found
*/
async function findFolderByPathOrId(
pathOrId: string,
): Promise<chrome.bookmarks.BookmarkTreeNode | null> {
try {
const nodes = await chrome.bookmarks.get(pathOrId);
if (nodes && nodes.length > 0 && !nodes[0].url) {
return nodes[0];
}
} catch (e) {
// do nothing, try to parse as path string
}
const pathParts = pathOrId
.split('/')
.map((p) => p.trim())
.filter((p) => p.length > 0);
if (pathParts.length === 0) return null;
const rootChildren = await chrome.bookmarks.getChildren('0');
let currentNodes = rootChildren;
let foundFolder: chrome.bookmarks.BookmarkTreeNode | null = null;
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i];
foundFolder = null;
let matchedNodeThisLevel: chrome.bookmarks.BookmarkTreeNode | null = null;
for (const node of currentNodes) {
if (!node.url && node.title.toLowerCase() === part.toLowerCase()) {
matchedNodeThisLevel = node;
break;
}
}
if (matchedNodeThisLevel) {
if (i === pathParts.length - 1) {
foundFolder = matchedNodeThisLevel;
} else {
currentNodes = await chrome.bookmarks.getChildren(matchedNodeThisLevel.id);
}
} else {
return null;
}
}
return foundFolder;
}
/**
* Create folder path (if it doesn't exist)
* @param folderPath Folder path string (e.g., "Work/Projects/Subproject")
* @param parentId Optional parent folder ID, defaults to "Bookmarks Bar"
* @returns Returns the created or found final folder node
*/
async function createFolderPath(
folderPath: string,
parentId?: string,
): Promise<chrome.bookmarks.BookmarkTreeNode> {
const pathParts = folderPath
.split('/')
.map((p) => p.trim())
.filter((p) => p.length > 0);
if (pathParts.length === 0) {
throw new Error('Folder path cannot be empty');
}
// If no parent ID specified, use "Bookmarks Bar" folder
let currentParentId: string = parentId || '';
if (!currentParentId) {
const rootChildren = await chrome.bookmarks.getChildren('0');
// Find "Bookmarks Bar" folder (usually ID is '1', but search by title for compatibility)
const bookmarkBarFolder = rootChildren.find(
(node) =>
!node.url &&
(node.title === getMessage('bookmarksBarLabel') ||
node.title === 'Bookmarks bar' ||
node.title === 'Bookmarks Bar'),
);
currentParentId = bookmarkBarFolder?.id || '1'; // fallback to default ID
}
let currentFolder: chrome.bookmarks.BookmarkTreeNode | null = null;
// Create or find folders level by level
for (const folderName of pathParts) {
const children: chrome.bookmarks.BookmarkTreeNode[] =
await chrome.bookmarks.getChildren(currentParentId);
// Check if folder with same name already exists
const existingFolder: chrome.bookmarks.BookmarkTreeNode | undefined = children.find(
(child: chrome.bookmarks.BookmarkTreeNode) =>
!child.url && child.title.toLowerCase() === folderName.toLowerCase(),
);
if (existingFolder) {
currentFolder = existingFolder;
currentParentId = existingFolder.id;
} else {
// Create new folder
currentFolder = await chrome.bookmarks.create({
parentId: currentParentId,
title: folderName,
});
currentParentId = currentFolder.id;
}
}
if (!currentFolder) {
throw new Error('Failed to create folder path');
}
return currentFolder;
}
/**
* Flatten bookmark tree (or node array) to bookmark list (excluding folders)
* @param nodes Bookmark tree nodes to flatten
* @returns Returns actual bookmark node array (nodes with URLs)
*/
function flattenBookmarkNodesToBookmarks(
nodes: chrome.bookmarks.BookmarkTreeNode[],
): chrome.bookmarks.BookmarkTreeNode[] {
const result: chrome.bookmarks.BookmarkTreeNode[] = [];
const stack = [...nodes]; // Use stack for iterative traversal to avoid deep recursion issues
while (stack.length > 0) {
const node = stack.pop();
if (!node) continue;
if (node.url) {
// It's a bookmark
result.push(node);
}
if (node.children) {
// Add child nodes to stack for processing
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
return result;
}
/**
* Find bookmarks by URL and title
* @param url Bookmark URL
* @param title Optional bookmark title for auxiliary matching
* @returns Returns array of matching bookmarks
*/
async function findBookmarksByUrl(
url: string,
title?: string,
): Promise<chrome.bookmarks.BookmarkTreeNode[]> {
// Use Chrome API to search by URL
const searchResults = await chrome.bookmarks.search({ url });
if (!title) {
return searchResults;
}
// If title is provided, further filter results
const titleLower = title.toLowerCase();
return searchResults.filter(
(bookmark) => bookmark.title && bookmark.title.toLowerCase().includes(titleLower),
);
}
/**
* Bookmark search tool
* Used to search bookmarks in Chrome browser
*/
class BookmarkSearchTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.BOOKMARK_SEARCH;
/**
* Execute bookmark search
*/
async execute(args: BookmarkSearchToolParams): Promise<ToolResult> {
const { query = '', maxResults = 50, folderPath } = args;
console.log(
`BookmarkSearchTool: Searching bookmarks, keywords: "${query}", folder path: "${folderPath}"`,
);
try {
let bookmarksToSearch: chrome.bookmarks.BookmarkTreeNode[] = [];
let targetFolderNode: chrome.bookmarks.BookmarkTreeNode | null = null;
// If folder path is specified, find that folder first
if (folderPath) {
targetFolderNode = await findFolderByPathOrId(folderPath);
if (!targetFolderNode) {
return createErrorResponse(`Specified folder not found: "${folderPath}"`);
}
// Get all bookmarks in that folder and its subfolders
const subTree = await chrome.bookmarks.getSubTree(targetFolderNode.id);
bookmarksToSearch =
subTree.length > 0 ? flattenBookmarkNodesToBookmarks(subTree[0].children || []) : [];
}
let filteredBookmarks: chrome.bookmarks.BookmarkTreeNode[];
if (query) {
if (targetFolderNode) {
// Has query keywords and specified folder: manually filter bookmarks from folder
const lowerCaseQuery = query.toLowerCase();
filteredBookmarks = bookmarksToSearch.filter(
(bookmark) =>
(bookmark.title && bookmark.title.toLowerCase().includes(lowerCaseQuery)) ||
(bookmark.url && bookmark.url.toLowerCase().includes(lowerCaseQuery)),
);
} else {
// Has query keywords but no specified folder: use API search
filteredBookmarks = await chrome.bookmarks.search({ query });
// API search may return folders (if title matches), filter them out
filteredBookmarks = filteredBookmarks.filter((item) => !!item.url);
}
} else {
// No query keywords
if (!targetFolderNode) {
// No folder path specified, get all bookmarks
const tree = await chrome.bookmarks.getTree();
bookmarksToSearch = flattenBookmarkNodesToBookmarks(tree);
}
filteredBookmarks = bookmarksToSearch;
}
// Limit number of results
const limitedResults = filteredBookmarks.slice(0, maxResults);
// Add folder path information for each bookmark
const resultsWithPath = await Promise.all(
limitedResults.map(async (bookmark) => {
const path = await getBookmarkFolderPath(bookmark.id);
return {
id: bookmark.id,
title: bookmark.title,
url: bookmark.url,
dateAdded: bookmark.dateAdded,
folderPath: path,
};
}),
);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
totalResults: resultsWithPath.length,
query: query || null,
folderSearched: targetFolderNode
? targetFolderNode.title || targetFolderNode.id
: 'All bookmarks',
bookmarks: resultsWithPath,
},
null,
2,
),
},
],
isError: false,
};
} catch (error) {
console.error('Error searching bookmarks:', error);
return createErrorResponse(
`Error searching bookmarks: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
/**
* Bookmark add tool
* Used to add new bookmarks to Chrome browser
*/
class BookmarkAddTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.BOOKMARK_ADD;
/**
* Execute add bookmark operation
*/
async execute(args: BookmarkAddToolParams): Promise<ToolResult> {
const { url, title, parentId, createFolder = false } = args;
console.log(`BookmarkAddTool: Adding bookmark, options:`, args);
try {
// If no URL provided, use current active tab
let bookmarkUrl = url;
let bookmarkTitle = title;
if (!bookmarkUrl) {
// Get current active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0] || !tabs[0].url) {
// tab.url might be undefined (e.g., chrome:// pages)
return createErrorResponse('No active tab with valid URL found, and no URL provided');
}
bookmarkUrl = tabs[0].url;
if (!bookmarkTitle) {
bookmarkTitle = tabs[0].title || bookmarkUrl; // If tab title is empty, use URL as title
}
}
if (!bookmarkUrl) {
// Should have been caught above, but as a safety measure
return createErrorResponse('URL is required to create bookmark');
}
// Parse parentId (could be ID or path string)
let actualParentId: string | undefined = undefined;
if (parentId) {
let folderNode = await findFolderByPathOrId(parentId);
if (!folderNode && createFolder) {
// If folder doesn't exist and creation is allowed, create folder path
try {
folderNode = await createFolderPath(parentId);
} catch (createError) {
return createErrorResponse(
`Failed to create folder path: ${createError instanceof Error ? createError.message : String(createError)}`,
);
}
}
if (folderNode) {
actualParentId = folderNode.id;
} else {
// Check if parentId might be a direct ID missed by findFolderByPathOrId (e.g., root folder '1')
try {
const nodes = await chrome.bookmarks.get(parentId);
if (nodes && nodes.length > 0 && !nodes[0].url) {
actualParentId = nodes[0].id;
} else {
return createErrorResponse(
`Specified parent folder (ID/path: "${parentId}") not found or is not a folder${createFolder ? ', and creation failed' : '. You can set createFolder=true to auto-create folders'}`,
);
}
} catch (e) {
return createErrorResponse(
`Specified parent folder (ID/path: "${parentId}") not found or invalid${createFolder ? ', and creation failed' : '. You can set createFolder=true to auto-create folders'}`,
);
}
}
} else {
// If no parentId specified, default to "Bookmarks Bar"
const rootChildren = await chrome.bookmarks.getChildren('0');
const bookmarkBarFolder = rootChildren.find(
(node) =>
!node.url &&
(node.title === getMessage('bookmarksBarLabel') ||
node.title === 'Bookmarks bar' ||
node.title === 'Bookmarks Bar'),
);
actualParentId = bookmarkBarFolder?.id || '1'; // fallback to default ID
}
// If actualParentId is still undefined, chrome.bookmarks.create will use default "Other Bookmarks", but we've set Bookmarks Bar
// Create bookmark
const newBookmark = await chrome.bookmarks.create({
parentId: actualParentId, // If undefined, API uses default value
title: bookmarkTitle || bookmarkUrl, // Ensure title is never empty
url: bookmarkUrl,
});
// Get bookmark path
const path = await getBookmarkFolderPath(newBookmark.id);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Bookmark added successfully',
bookmark: {
id: newBookmark.id,
title: newBookmark.title,
url: newBookmark.url,
dateAdded: newBookmark.dateAdded,
folderPath: path,
},
folderCreated: createFolder && parentId ? 'Folder created if necessary' : false,
},
null,
2,
),
},
],
isError: false,
};
} catch (error) {
console.error('Error adding bookmark:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
// Provide more specific error messages for common error cases, such as trying to bookmark chrome:// URLs
if (errorMessage.includes("Can't bookmark URLs of type")) {
return createErrorResponse(
`Error adding bookmark: Cannot bookmark this type of URL (e.g., chrome:// system pages). ${errorMessage}`,
);
}
return createErrorResponse(`Error adding bookmark: ${errorMessage}`);
}
}
}
/**
* Bookmark delete tool
* Used to delete bookmarks in Chrome browser
*/
class BookmarkDeleteTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.BOOKMARK_DELETE;
/**
* Execute delete bookmark operation
*/
async execute(args: BookmarkDeleteToolParams): Promise<ToolResult> {
const { bookmarkId, url, title } = args;
console.log(`BookmarkDeleteTool: Deleting bookmark, options:`, args);
if (!bookmarkId && !url) {
return createErrorResponse('Must provide bookmark ID or URL to delete bookmark');
}
try {
let bookmarksToDelete: chrome.bookmarks.BookmarkTreeNode[] = [];
if (bookmarkId) {
// Delete by ID
try {
const nodes = await chrome.bookmarks.get(bookmarkId);
if (nodes && nodes.length > 0 && nodes[0].url) {
bookmarksToDelete = nodes;
} else {
return createErrorResponse(
`Bookmark with ID "${bookmarkId}" not found, or the ID does not correspond to a bookmark`,
);
}
} catch (error) {
return createErrorResponse(`Invalid bookmark ID: "${bookmarkId}"`);
}
} else if (url) {
// Delete by URL
bookmarksToDelete = await findBookmarksByUrl(url, title);
if (bookmarksToDelete.length === 0) {
return createErrorResponse(
`No bookmark found with URL "${url}"${title ? ` (title contains: "${title}")` : ''}`,
);
}
}
// Delete found bookmarks
const deletedBookmarks = [];
const errors = [];
for (const bookmark of bookmarksToDelete) {
try {
// Get path information before deletion
const path = await getBookmarkFolderPath(bookmark.id);
await chrome.bookmarks.remove(bookmark.id);
deletedBookmarks.push({
id: bookmark.id,
title: bookmark.title,
url: bookmark.url,
folderPath: path,
});
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
errors.push(
`Failed to delete bookmark "${bookmark.title}" (ID: ${bookmark.id}): ${errorMsg}`,
);
}
}
if (deletedBookmarks.length === 0) {
return createErrorResponse(`Failed to delete bookmarks: ${errors.join('; ')}`);
}
const result: any = {
success: true,
message: `Successfully deleted ${deletedBookmarks.length} bookmark(s)`,
deletedBookmarks,
};
if (errors.length > 0) {
result.partialSuccess = true;
result.errors = errors;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
console.error('Error deleting bookmark:', error);
return createErrorResponse(
`Error deleting bookmark: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const bookmarkSearchTool = new BookmarkSearchTool();
export const bookmarkAddTool = new BookmarkAddTool();
export const bookmarkDeleteTool = new BookmarkDeleteTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/workers/ort-wasm-simd-threaded.mjs:
--------------------------------------------------------------------------------
```
var ortWasmThreaded = (() => {
var _scriptName = import.meta.url;
return (
async function(moduleArg = {}) {
var moduleRtn;
var f=moduleArg,aa,ba,ca=new Promise((a,b)=>{aa=a;ba=b}),da="object"==typeof window,k="undefined"!=typeof WorkerGlobalScope,l="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node&&"renderer"!=process.type,m=k&&self.name?.startsWith("em-pthread");if(l){const {createRequire:a}=await import("module");var require=a(import.meta.url),n=require("worker_threads");global.Worker=n.Worker;m=(k=!n.jb)&&"em-pthread"==n.workerData}
f.mountExternalData=(a,b)=>{a.startsWith("./")&&(a=a.substring(2));(f.Sa||(f.Sa=new Map)).set(a,b)};f.unmountExternalData=()=>{delete f.Sa};var SharedArrayBuffer=globalThis.SharedArrayBuffer??(new WebAssembly.Memory({initial:0,maximum:0,lb:!0})).buffer.constructor,ea=Object.assign({},f),fa="./this.program",q=(a,b)=>{throw b;},r="",ha,t;
if(l){var fs=require("fs"),ia=require("path");import.meta.url.startsWith("data:")||(r=ia.dirname(require("url").fileURLToPath(import.meta.url))+"/");t=a=>{a=u(a)?new URL(a):a;return fs.readFileSync(a)};ha=async a=>{a=u(a)?new URL(a):a;return fs.readFileSync(a,void 0)};!f.thisProgram&&1<process.argv.length&&(fa=process.argv[1].replace(/\\/g,"/"));process.argv.slice(2);q=(a,b)=>{process.exitCode=a;throw b;}}else if(da||k)k?r=self.location.href:"undefined"!=typeof document&&document.currentScript&&
(r=document.currentScript.src),_scriptName&&(r=_scriptName),r.startsWith("blob:")?r="":r=r.slice(0,r.replace(/[?#].*/,"").lastIndexOf("/")+1),l||(k&&(t=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),ha=async a=>{if(u(a))return new Promise((c,d)=>{var e=new XMLHttpRequest;e.open("GET",a,!0);e.responseType="arraybuffer";e.onload=()=>{200==e.status||0==e.status&&e.response?c(e.response):d(e.status)};e.onerror=d;e.send(null)});
var b=await fetch(a,{credentials:"same-origin"});if(b.ok)return b.arrayBuffer();throw Error(b.status+" : "+b.url);});var ja=console.log.bind(console),ka=console.error.bind(console);l&&(ja=(...a)=>fs.writeSync(1,a.join(" ")+"\n"),ka=(...a)=>fs.writeSync(2,a.join(" ")+"\n"));var la=ja,w=ka;Object.assign(f,ea);ea=null;var x=f.wasmBinary,y,ma,z=!1,A,B,na,oa,pa,qa,ra,C,sa,u=a=>a.startsWith("file://");function D(){y.buffer!=B.buffer&&E();return B}function F(){y.buffer!=B.buffer&&E();return na}
function ta(){y.buffer!=B.buffer&&E();return oa}function G(){y.buffer!=B.buffer&&E();return pa}function H(){y.buffer!=B.buffer&&E();return qa}function va(){y.buffer!=B.buffer&&E();return ra}function I(){y.buffer!=B.buffer&&E();return sa}
if(m){var wa;if(l){var xa=n.parentPort;xa.on("message",b=>onmessage({data:b}));Object.assign(globalThis,{self:global,postMessage:b=>xa.postMessage(b)})}var ya=!1;w=function(...b){b=b.join(" ");l?fs.writeSync(2,b+"\n"):console.error(b)};self.alert=function(...b){postMessage({Ra:"alert",text:b.join(" "),eb:J()})};self.onunhandledrejection=b=>{throw b.reason||b;};function a(b){try{var c=b.data,d=c.Ra;if("load"===d){let e=[];self.onmessage=g=>e.push(g);self.startWorker=()=>{postMessage({Ra:"loaded"});
for(let g of e)a(g);self.onmessage=a};for(const g of c.Za)if(!f[g]||f[g].proxy)f[g]=(...h)=>{postMessage({Ra:"callHandler",Ya:g,args:h})},"print"==g&&(la=f[g]),"printErr"==g&&(w=f[g]);y=c.gb;E();wa(c.hb)}else if("run"===d){za(c.Qa);Aa(c.Qa,0,0,1,0,0);Ba();Ca(c.Qa);ya||=!0;try{Da(c.bb,c.Va)}catch(e){if("unwind"!=e)throw e;}}else"setimmediate"!==c.target&&("checkMailbox"===d?ya&&K():d&&(w(`worker: received unknown command ${d}`),w(c)))}catch(e){throw Ea(),e;}}self.onmessage=a}
function E(){var a=y.buffer;f.HEAP8=B=new Int8Array(a);f.HEAP16=oa=new Int16Array(a);f.HEAPU8=na=new Uint8Array(a);f.HEAPU16=new Uint16Array(a);f.HEAP32=pa=new Int32Array(a);f.HEAPU32=qa=new Uint32Array(a);f.HEAPF32=ra=new Float32Array(a);f.HEAPF64=sa=new Float64Array(a);f.HEAP64=C=new BigInt64Array(a);f.HEAPU64=new BigUint64Array(a)}m||(y=new WebAssembly.Memory({initial:256,maximum:65536,shared:!0}),E());function Fa(){m?startWorker(f):L.$()}var M=0,N=null;
function Ga(){M--;if(0==M&&N){var a=N;N=null;a()}}function O(a){a="Aborted("+a+")";w(a);z=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");ba(a);throw a;}var Ha;async function Ia(a){if(!x)try{var b=await ha(a);return new Uint8Array(b)}catch{}if(a==Ha&&x)a=new Uint8Array(x);else if(t)a=t(a);else throw"both async and sync fetching of the wasm failed";return a}
async function Ja(a,b){try{var c=await Ia(a);return await WebAssembly.instantiate(c,b)}catch(d){w(`failed to asynchronously prepare wasm: ${d}`),O(d)}}async function Ka(a){var b=Ha;if(!x&&"function"==typeof WebAssembly.instantiateStreaming&&!u(b)&&!l)try{var c=fetch(b,{credentials:"same-origin"});return await WebAssembly.instantiateStreaming(c,a)}catch(d){w(`wasm streaming compile failed: ${d}`),w("falling back to ArrayBuffer instantiation")}return Ja(b,a)}
function La(){Ma={j:Na,b:Oa,E:Pa,f:Qa,U:Ra,A:Sa,C:Ta,V:Ua,S:Va,L:Wa,R:Xa,n:Ya,B:Za,y:$a,T:ab,z:bb,_:cb,O:db,w:eb,F:fb,t:gb,i:hb,N:Ca,X:ib,I:jb,J:kb,K:lb,G:mb,H:nb,u:ob,q:pb,Z:qb,o:rb,k:sb,Y:tb,d:ub,W:vb,x:wb,c:xb,e:yb,h:zb,v:Ab,s:Bb,r:Cb,P:Db,Q:Eb,D:Fb,g:Gb,m:Hb,M:Ib,l:Jb,a:y,p:Kb};return{a:Ma}}
var Mb={802156:(a,b,c,d,e)=>{if("undefined"==typeof f||!f.Sa)return 1;a=Lb(Number(a>>>0));a.startsWith("./")&&(a=a.substring(2));a=f.Sa.get(a);if(!a)return 2;b=Number(b>>>0);c=Number(c>>>0);d=Number(d>>>0);if(b+c>a.byteLength)return 3;try{const g=a.subarray(b,b+c);switch(e){case 0:F().set(g,d>>>0);break;case 1:f.ib?f.ib(d,g):f.kb(d,g);break;default:return 4}return 0}catch{return 4}},802980:()=>"undefined"!==typeof wasmOffsetConverter};function Na(){return"undefined"!==typeof wasmOffsetConverter}
class Nb{name="ExitStatus";constructor(a){this.message=`Program terminated with exit(${a})`;this.status=a}}
var Ob=a=>{a.terminate();a.onmessage=()=>{}},Pb=[],Sb=a=>{0==Q.length&&(Qb(),Rb(Q[0]));var b=Q.pop();if(!b)return 6;R.push(b);S[a.Qa]=b;b.Qa=a.Qa;var c={Ra:"run",bb:a.ab,Va:a.Va,Qa:a.Qa};l&&b.unref();b.postMessage(c,a.Xa);return 0},T=0,V=(a,b,...c)=>{for(var d=2*c.length,e=Tb(),g=Ub(8*d),h=g>>>3,p=0;p<c.length;p++){var v=c[p];"bigint"==typeof v?(C[h+2*p]=1n,C[h+2*p+1]=v):(C[h+2*p]=0n,I()[h+2*p+1>>>0]=v)}a=Vb(a,0,d,g,b);U(e);return a};
function Kb(a){if(m)return V(0,1,a);A=a;if(!(0<T)){for(var b of R)Ob(b);for(b of Q)Ob(b);Q=[];R=[];S={};z=!0}q(a,new Nb(a))}function Wb(a){if(m)return V(1,0,a);Fb(a)}var Fb=a=>{A=a;if(m)throw Wb(a),"unwind";Kb(a)},Q=[],R=[],Xb=[],S={};function Yb(){for(var a=f.numThreads-1;a--;)Qb();Pb.unshift(()=>{M++;Zb(()=>Ga())})}var ac=a=>{var b=a.Qa;delete S[b];Q.push(a);R.splice(R.indexOf(a),1);a.Qa=0;$b(b)};function Ba(){Xb.forEach(a=>a())}
var Rb=a=>new Promise(b=>{a.onmessage=g=>{g=g.data;var h=g.Ra;if(g.Ta&&g.Ta!=J()){var p=S[g.Ta];p?p.postMessage(g,g.Xa):w(`Internal error! Worker sent a message "${h}" to target pthread ${g.Ta}, but that thread no longer exists!`)}else if("checkMailbox"===h)K();else if("spawnThread"===h)Sb(g);else if("cleanupThread"===h)ac(S[g.cb]);else if("loaded"===h)a.loaded=!0,l&&!a.Qa&&a.unref(),b(a);else if("alert"===h)alert(`Thread ${g.eb}: ${g.text}`);else if("setimmediate"===g.target)a.postMessage(g);else if("callHandler"===
h)f[g.Ya](...g.args);else h&&w(`worker sent an unknown command ${h}`)};a.onerror=g=>{w(`${"worker sent an error!"} ${g.filename}:${g.lineno}: ${g.message}`);throw g;};l&&(a.on("message",g=>a.onmessage({data:g})),a.on("error",g=>a.onerror(g)));var c=[],d=[],e;for(e of d)f.propertyIsEnumerable(e)&&c.push(e);a.postMessage({Ra:"load",Za:c,gb:y,hb:ma})});function Zb(a){m?a():Promise.all(Q.map(Rb)).then(a)}
function Qb(){var a=new Worker(new URL(import.meta.url),{type:"module",workerData:"em-pthread",name:"em-pthread"});Q.push(a)}var za=a=>{E();var b=H()[a+52>>>2>>>0];a=H()[a+56>>>2>>>0];bc(b,b-a);U(b)},W=[],cc,Da=(a,b)=>{T=0;var c=W[a];c||(a>=W.length&&(W.length=a+1),W[a]=c=cc.get(a));a=c(b);0<T?A=a:dc(a)};class ec{constructor(a){this.Ua=a-24}}var fc=0,gc=0;
function Oa(a,b,c){a>>>=0;var d=new ec(a);b>>>=0;c>>>=0;H()[d.Ua+16>>>2>>>0]=0;H()[d.Ua+4>>>2>>>0]=b;H()[d.Ua+8>>>2>>>0]=c;fc=a;gc++;throw fc;}function hc(a,b,c,d){return m?V(2,1,a,b,c,d):Pa(a,b,c,d)}function Pa(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;if("undefined"==typeof SharedArrayBuffer)return 6;var e=[];if(m&&0===e.length)return hc(a,b,c,d);a={ab:c,Qa:a,Va:d,Xa:e};return m?(a.Ra="spawnThread",postMessage(a,e),0):Sb(a)}
var ic="undefined"!=typeof TextDecoder?new TextDecoder:void 0,jc=(a,b=0,c=NaN)=>{b>>>=0;var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16<c-b&&a.buffer&&ic)return ic.decode(a.buffer instanceof ArrayBuffer?a.subarray(b,c):a.slice(b,c));for(d="";b<c;){var e=a[b++];if(e&128){var g=a[b++]&63;if(192==(e&224))d+=String.fromCharCode((e&31)<<6|g);else{var h=a[b++]&63;e=224==(e&240)?(e&15)<<12|g<<6|h:(e&7)<<18|g<<12|h<<6|a[b++]&63;65536>e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|
e&1023))}}else d+=String.fromCharCode(e)}return d},Lb=(a,b)=>(a>>>=0)?jc(F(),a,b):"";function Qa(a,b,c){return m?V(3,1,a,b,c):0}function Ra(a,b){if(m)return V(4,1,a,b)}
var X=(a,b,c)=>{var d=F();b>>>=0;if(0<c){var e=b;c=b+c-1;for(var g=0;g<a.length;++g){var h=a.charCodeAt(g);if(55296<=h&&57343>=h){var p=a.charCodeAt(++g);h=65536+((h&1023)<<10)|p&1023}if(127>=h){if(b>=c)break;d[b++>>>0]=h}else{if(2047>=h){if(b+1>=c)break;d[b++>>>0]=192|h>>6}else{if(65535>=h){if(b+2>=c)break;d[b++>>>0]=224|h>>12}else{if(b+3>=c)break;d[b++>>>0]=240|h>>18;d[b++>>>0]=128|h>>12&63}d[b++>>>0]=128|h>>6&63}d[b++>>>0]=128|h&63}}d[b>>>0]=0;a=b-e}else a=0;return a};
function Sa(a,b){if(m)return V(5,1,a,b)}function Ta(a,b,c){if(m)return V(6,1,a,b,c)}function Ua(a,b,c){return m?V(7,1,a,b,c):0}function Va(a,b){if(m)return V(8,1,a,b)}function Wa(a,b,c){if(m)return V(9,1,a,b,c)}function Xa(a,b,c,d){if(m)return V(10,1,a,b,c,d)}function Ya(a,b,c,d){if(m)return V(11,1,a,b,c,d)}function Za(a,b,c,d){if(m)return V(12,1,a,b,c,d)}function $a(a){if(m)return V(13,1,a)}function ab(a,b){if(m)return V(14,1,a,b)}function bb(a,b,c){if(m)return V(15,1,a,b,c)}var cb=()=>O("");
function db(a){Aa(a>>>0,!k,1,!da,131072,!1);Ba()}var kc=a=>{if(!z)try{if(a(),!(0<T))try{m?dc(A):Fb(A)}catch(b){b instanceof Nb||"unwind"==b||q(1,b)}}catch(b){b instanceof Nb||"unwind"==b||q(1,b)}};function Ca(a){a>>>=0;"function"===typeof Atomics.fb&&(Atomics.fb(G(),a>>>2,a).value.then(K),a+=128,Atomics.store(G(),a>>>2,1))}var K=()=>{var a=J();a&&(Ca(a),kc(lc))};function eb(a,b){a>>>=0;a==b>>>0?setTimeout(K):m?postMessage({Ta:a,Ra:"checkMailbox"}):(a=S[a])&&a.postMessage({Ra:"checkMailbox"})}
var mc=[];function fb(a,b,c,d,e){b>>>=0;d/=2;mc.length=d;c=e>>>0>>>3;for(e=0;e<d;e++)mc[e]=C[c+2*e]?C[c+2*e+1]:I()[c+2*e+1>>>0];return(b?Mb[b]:nc[a])(...mc)}var gb=()=>{T=0};function hb(a){a>>>=0;m?postMessage({Ra:"cleanupThread",cb:a}):ac(S[a])}function ib(a){l&&S[a>>>0].ref()}
function jb(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);G()[b>>>2>>>0]=a.getUTCSeconds();G()[b+4>>>2>>>0]=a.getUTCMinutes();G()[b+8>>>2>>>0]=a.getUTCHours();G()[b+12>>>2>>>0]=a.getUTCDate();G()[b+16>>>2>>>0]=a.getUTCMonth();G()[b+20>>>2>>>0]=a.getUTCFullYear()-1900;G()[b+24>>>2>>>0]=a.getUTCDay();a=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0;G()[b+28>>>2>>>0]=a}
var oc=a=>0===a%4&&(0!==a%100||0===a%400),pc=[0,31,60,91,121,152,182,213,244,274,305,335],qc=[0,31,59,90,120,151,181,212,243,273,304,334];
function kb(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);G()[b>>>2>>>0]=a.getSeconds();G()[b+4>>>2>>>0]=a.getMinutes();G()[b+8>>>2>>>0]=a.getHours();G()[b+12>>>2>>>0]=a.getDate();G()[b+16>>>2>>>0]=a.getMonth();G()[b+20>>>2>>>0]=a.getFullYear()-1900;G()[b+24>>>2>>>0]=a.getDay();var c=(oc(a.getFullYear())?pc:qc)[a.getMonth()]+a.getDate()-1|0;G()[b+28>>>2>>>0]=c;G()[b+36>>>2>>>0]=-(60*a.getTimezoneOffset());c=(new Date(a.getFullYear(),6,1)).getTimezoneOffset();
var d=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();a=(c!=d&&a.getTimezoneOffset()==Math.min(d,c))|0;G()[b+32>>>2>>>0]=a}
function lb(a){a>>>=0;var b=new Date(G()[a+20>>>2>>>0]+1900,G()[a+16>>>2>>>0],G()[a+12>>>2>>>0],G()[a+8>>>2>>>0],G()[a+4>>>2>>>0],G()[a>>>2>>>0],0),c=G()[a+32>>>2>>>0],d=b.getTimezoneOffset(),e=(new Date(b.getFullYear(),6,1)).getTimezoneOffset(),g=(new Date(b.getFullYear(),0,1)).getTimezoneOffset(),h=Math.min(g,e);0>c?G()[a+32>>>2>>>0]=Number(e!=g&&h==d):0<c!=(h==d)&&(e=Math.max(g,e),b.setTime(b.getTime()+6E4*((0<c?h:e)-d)));G()[a+24>>>2>>>0]=b.getDay();c=(oc(b.getFullYear())?pc:qc)[b.getMonth()]+
b.getDate()-1|0;G()[a+28>>>2>>>0]=c;G()[a>>>2>>>0]=b.getSeconds();G()[a+4>>>2>>>0]=b.getMinutes();G()[a+8>>>2>>>0]=b.getHours();G()[a+12>>>2>>>0]=b.getDate();G()[a+16>>>2>>>0]=b.getMonth();G()[a+20>>>2>>>0]=b.getYear();a=b.getTime();return BigInt(isNaN(a)?-1:a/1E3)}function mb(a,b,c,d,e,g,h){return m?V(16,1,a,b,c,d,e,g,h):-52}function nb(a,b,c,d,e,g){if(m)return V(17,1,a,b,c,d,e,g)}var Y={},xb=()=>performance.timeOrigin+performance.now();
function ob(a,b){if(m)return V(18,1,a,b);Y[a]&&(clearTimeout(Y[a].id),delete Y[a]);if(!b)return 0;var c=setTimeout(()=>{delete Y[a];kc(()=>rc(a,performance.timeOrigin+performance.now()))},b);Y[a]={id:c,mb:b};return 0}
function pb(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;var e=(new Date).getFullYear(),g=(new Date(e,0,1)).getTimezoneOffset();e=(new Date(e,6,1)).getTimezoneOffset();var h=Math.max(g,e);H()[a>>>2>>>0]=60*h;G()[b>>>2>>>0]=Number(g!=e);b=p=>{var v=Math.abs(p);return`UTC${0<=p?"-":"+"}${String(Math.floor(v/60)).padStart(2,"0")}${String(v%60).padStart(2,"0")}`};a=b(g);b=b(e);e<g?(X(a,c,17),X(b,d,17)):(X(a,d,17),X(b,c,17))}var tb=()=>Date.now(),sc=1;
function qb(a,b,c){if(!(0<=a&&3>=a))return 28;if(0===a)a=Date.now();else if(sc)a=performance.timeOrigin+performance.now();else return 52;C[c>>>0>>>3]=BigInt(Math.round(1E6*a));return 0}var tc=[];function rb(a,b,c){a>>>=0;b>>>=0;c>>>=0;tc.length=0;for(var d;d=F()[b++>>>0];){var e=105!=d;e&=112!=d;c+=e&&c%8?4:0;tc.push(112==d?H()[c>>>2>>>0]:106==d?C[c>>>3]:105==d?G()[c>>>2>>>0]:I()[c>>>3>>>0]);c+=e?8:4}return Mb[a](...tc)}var sb=()=>{};function ub(a,b){return w(Lb(a>>>0,b>>>0))}
var vb=()=>{T+=1;throw"unwind";};function wb(){return 4294901760}var yb=()=>l?require("os").cpus().length:navigator.hardwareConcurrency;function zb(){O("Cannot use emscripten_pc_get_function without -sUSE_OFFSET_CONVERTER");return 0}
function Ab(a){a>>>=0;var b=F().length;if(a<=b||4294901760<a)return!1;for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);a:{d=(Math.min(4294901760,65536*Math.ceil(Math.max(a,d)/65536))-y.buffer.byteLength+65535)/65536|0;try{y.grow(d);E();var e=1;break a}catch(g){}e=void 0}if(e)return!0}return!1}var uc=()=>{O("Cannot use convertFrameToPC (needed by __builtin_return_address) without -sUSE_OFFSET_CONVERTER");return 0},Z={},vc=a=>{a.forEach(b=>{var c=uc();c&&(Z[c]=b)})};
function Bb(){var a=Error().stack.toString().split("\n");"Error"==a[0]&&a.shift();vc(a);Z.Wa=uc();Z.$a=a;return Z.Wa}function Cb(a,b,c){a>>>=0;b>>>=0;if(Z.Wa==a)var d=Z.$a;else d=Error().stack.toString().split("\n"),"Error"==d[0]&&d.shift(),vc(d);for(var e=3;d[e]&&uc()!=a;)++e;for(a=0;a<c&&d[a+e];++a)G()[b+4*a>>>2>>>0]=uc();return a}
var wc={},yc=()=>{if(!xc){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:fa||"./this.program"},b;for(b in wc)void 0===wc[b]?delete a[b]:a[b]=wc[b];var c=[];for(b in a)c.push(`${b}=${a[b]}`);xc=c}return xc},xc;
function Db(a,b){if(m)return V(19,1,a,b);a>>>=0;b>>>=0;var c=0;yc().forEach((d,e)=>{var g=b+c;e=H()[a+4*e>>>2>>>0]=g;for(g=0;g<d.length;++g)D()[e++>>>0]=d.charCodeAt(g);D()[e>>>0]=0;c+=d.length+1});return 0}function Eb(a,b){if(m)return V(20,1,a,b);a>>>=0;b>>>=0;var c=yc();H()[a>>>2>>>0]=c.length;var d=0;c.forEach(e=>d+=e.length+1);H()[b>>>2>>>0]=d;return 0}function Gb(a){return m?V(21,1,a):52}function Hb(a,b,c,d){return m?V(22,1,a,b,c,d):52}function Ib(a,b,c,d){return m?V(23,1,a,b,c,d):70}
var zc=[null,[],[]];function Jb(a,b,c,d){if(m)return V(24,1,a,b,c,d);b>>>=0;c>>>=0;d>>>=0;for(var e=0,g=0;g<c;g++){var h=H()[b>>>2>>>0],p=H()[b+4>>>2>>>0];b+=8;for(var v=0;v<p;v++){var P=F()[h+v>>>0],ua=zc[a];0===P||10===P?((1===a?la:w)(jc(ua)),ua.length=0):ua.push(P)}e+=p}H()[d>>>2>>>0]=e;return 0}m||Yb();var nc=[Kb,Wb,hc,Qa,Ra,Sa,Ta,Ua,Va,Wa,Xa,Ya,Za,$a,ab,bb,mb,nb,ob,Db,Eb,Gb,Hb,Ib,Jb],Ma,L;
(async function(){function a(d,e){L=d.exports;L=Ac();Xb.push(L.Da);cc=L.Ea;ma=e;Ga();return L}M++;var b=La();if(f.instantiateWasm)return new Promise(d=>{f.instantiateWasm(b,(e,g)=>{a(e,g);d(e.exports)})});if(m)return new Promise(d=>{wa=e=>{var g=new WebAssembly.Instance(e,La());d(a(g,e))}});Ha??=f.locateFile?f.locateFile?f.locateFile("ort-wasm-simd-threaded.wasm",r):r+"ort-wasm-simd-threaded.wasm":(new URL("ort-wasm-simd-threaded.wasm",import.meta.url)).href;try{var c=await Ka(b);return a(c.instance,
c.module)}catch(d){return ba(d),Promise.reject(d)}})();f._OrtInit=(a,b)=>(f._OrtInit=L.aa)(a,b);f._OrtGetLastError=(a,b)=>(f._OrtGetLastError=L.ba)(a,b);f._OrtCreateSessionOptions=(a,b,c,d,e,g,h,p,v,P)=>(f._OrtCreateSessionOptions=L.ca)(a,b,c,d,e,g,h,p,v,P);f._OrtAppendExecutionProvider=(a,b,c,d,e)=>(f._OrtAppendExecutionProvider=L.da)(a,b,c,d,e);f._OrtAddFreeDimensionOverride=(a,b,c)=>(f._OrtAddFreeDimensionOverride=L.ea)(a,b,c);
f._OrtAddSessionConfigEntry=(a,b,c)=>(f._OrtAddSessionConfigEntry=L.fa)(a,b,c);f._OrtReleaseSessionOptions=a=>(f._OrtReleaseSessionOptions=L.ga)(a);f._OrtCreateSession=(a,b,c)=>(f._OrtCreateSession=L.ha)(a,b,c);f._OrtReleaseSession=a=>(f._OrtReleaseSession=L.ia)(a);f._OrtGetInputOutputCount=(a,b,c)=>(f._OrtGetInputOutputCount=L.ja)(a,b,c);f._OrtGetInputOutputMetadata=(a,b,c,d)=>(f._OrtGetInputOutputMetadata=L.ka)(a,b,c,d);f._OrtFree=a=>(f._OrtFree=L.la)(a);
f._OrtCreateTensor=(a,b,c,d,e,g)=>(f._OrtCreateTensor=L.ma)(a,b,c,d,e,g);f._OrtGetTensorData=(a,b,c,d,e)=>(f._OrtGetTensorData=L.na)(a,b,c,d,e);f._OrtReleaseTensor=a=>(f._OrtReleaseTensor=L.oa)(a);f._OrtCreateRunOptions=(a,b,c,d)=>(f._OrtCreateRunOptions=L.pa)(a,b,c,d);f._OrtAddRunConfigEntry=(a,b,c)=>(f._OrtAddRunConfigEntry=L.qa)(a,b,c);f._OrtReleaseRunOptions=a=>(f._OrtReleaseRunOptions=L.ra)(a);f._OrtCreateBinding=a=>(f._OrtCreateBinding=L.sa)(a);
f._OrtBindInput=(a,b,c)=>(f._OrtBindInput=L.ta)(a,b,c);f._OrtBindOutput=(a,b,c,d)=>(f._OrtBindOutput=L.ua)(a,b,c,d);f._OrtClearBoundOutputs=a=>(f._OrtClearBoundOutputs=L.va)(a);f._OrtReleaseBinding=a=>(f._OrtReleaseBinding=L.wa)(a);f._OrtRunWithBinding=(a,b,c,d,e)=>(f._OrtRunWithBinding=L.xa)(a,b,c,d,e);f._OrtRun=(a,b,c,d,e,g,h,p)=>(f._OrtRun=L.ya)(a,b,c,d,e,g,h,p);f._OrtEndProfiling=a=>(f._OrtEndProfiling=L.za)(a);var J=()=>(J=L.Aa)();f._free=a=>(f._free=L.Ba)(a);f._malloc=a=>(f._malloc=L.Ca)(a);
var Aa=(a,b,c,d,e,g)=>(Aa=L.Fa)(a,b,c,d,e,g),Ea=()=>(Ea=L.Ga)(),Vb=(a,b,c,d,e)=>(Vb=L.Ha)(a,b,c,d,e),$b=a=>($b=L.Ia)(a),dc=a=>(dc=L.Ja)(a),rc=(a,b)=>(rc=L.Ka)(a,b),lc=()=>(lc=L.La)(),bc=(a,b)=>(bc=L.Ma)(a,b),U=a=>(U=L.Na)(a),Ub=a=>(Ub=L.Oa)(a),Tb=()=>(Tb=L.Pa)();function Ac(){var a=L;a=Object.assign({},a);var b=d=>()=>d()>>>0,c=d=>e=>d(e)>>>0;a.Aa=b(a.Aa);a.Ca=c(a.Ca);a.Oa=c(a.Oa);a.Pa=b(a.Pa);a.__cxa_get_exception_ptr=c(a.__cxa_get_exception_ptr);return a}f.stackSave=()=>Tb();f.stackRestore=a=>U(a);
f.stackAlloc=a=>Ub(a);f.setValue=function(a,b,c="i8"){c.endsWith("*")&&(c="*");switch(c){case "i1":D()[a>>>0]=b;break;case "i8":D()[a>>>0]=b;break;case "i16":ta()[a>>>1>>>0]=b;break;case "i32":G()[a>>>2>>>0]=b;break;case "i64":C[a>>>3]=BigInt(b);break;case "float":va()[a>>>2>>>0]=b;break;case "double":I()[a>>>3>>>0]=b;break;case "*":H()[a>>>2>>>0]=b;break;default:O(`invalid type for setValue: ${c}`)}};
f.getValue=function(a,b="i8"){b.endsWith("*")&&(b="*");switch(b){case "i1":return D()[a>>>0];case "i8":return D()[a>>>0];case "i16":return ta()[a>>>1>>>0];case "i32":return G()[a>>>2>>>0];case "i64":return C[a>>>3];case "float":return va()[a>>>2>>>0];case "double":return I()[a>>>3>>>0];case "*":return H()[a>>>2>>>0];default:O(`invalid type for getValue: ${b}`)}};f.UTF8ToString=Lb;f.stringToUTF8=X;
f.lengthBytesUTF8=a=>{for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b};function Bc(){if(0<M)N=Bc;else if(m)aa(f),Fa();else{for(;0<Pb.length;)Pb.shift()(f);0<M?N=Bc:(f.calledRun=!0,z||(Fa(),aa(f)))}}Bc();f.PTR_SIZE=4;moduleRtn=ca;
return moduleRtn;
}
);
})();
export default ortWasmThreaded;
var isPthread = globalThis.self?.name?.startsWith('em-pthread');
var isNode = typeof globalThis.process?.versions?.node == 'string';
if (isNode) isPthread = (await import('worker_threads')).workerData === 'em-pthread';
// When running as a pthread, construct a new instance on startup
isPthread && ortWasmThreaded();
```