first commit

This commit is contained in:
nasir@endelospay.com
2025-08-12 02:54:17 +05:00
commit d97cad1736
225 changed files with 137626 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# Chrome Extension Private Key
# Copy this file to .env and replace with your actual private key
# This key is used for Chrome extension packaging and should be kept secure
CHROME_EXTENSION_KEY=YOUR_PRIVATE_KEY_HERE

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 hangwin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
# WXT + Vue 3
This template should help get you started developing with Vue 3 in WXT.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar).

View File

@@ -0,0 +1,446 @@
{
"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"
}
}

View File

@@ -0,0 +1,446 @@
{
"extensionName": {
"message": "chrome-mcp-server",
"description": "Extension name"
},
"extensionDescription": {
"message": "Exposes browser capabilities with your own chrome",
"description": "Extension description"
},
"nativeServerConfigLabel": {
"message": "Native Server Configuration",
"description": "Main section header for native server settings"
},
"semanticEngineLabel": {
"message": "Semantic Engine",
"description": "Main section header for semantic engine"
},
"embeddingModelLabel": {
"message": "Embedding Model",
"description": "Main section header for model selection"
},
"indexDataManagementLabel": {
"message": "Index Data Management",
"description": "Main section header for data management"
},
"modelCacheManagementLabel": {
"message": "Model Cache Management",
"description": "Main section header for cache management"
},
"statusLabel": {
"message": "Status",
"description": "Generic status label"
},
"runningStatusLabel": {
"message": "Running Status",
"description": "Server running status label"
},
"connectionStatusLabel": {
"message": "Connection Status",
"description": "Connection status label"
},
"lastUpdatedLabel": {
"message": "Last Updated:",
"description": "Last updated timestamp label"
},
"connectButton": {
"message": "Connect",
"description": "Connect button text"
},
"disconnectButton": {
"message": "Disconnect",
"description": "Disconnect button text"
},
"connectingStatus": {
"message": "Connecting...",
"description": "Connecting status message"
},
"connectedStatus": {
"message": "Connected",
"description": "Connected status message"
},
"disconnectedStatus": {
"message": "Disconnected",
"description": "Disconnected status message"
},
"detectingStatus": {
"message": "Detecting...",
"description": "Detecting status message"
},
"serviceRunningStatus": {
"message": "Service Running (Port: $PORT$)",
"description": "Service running with port number",
"placeholders": {
"port": {
"content": "$1",
"example": "12306"
}
}
},
"serviceNotConnectedStatus": {
"message": "Service Not Connected",
"description": "Service not connected status"
},
"connectedServiceNotStartedStatus": {
"message": "Connected, Service Not Started",
"description": "Connected but service not started status"
},
"mcpServerConfigLabel": {
"message": "MCP Server Configuration",
"description": "MCP server configuration section label"
},
"connectionPortLabel": {
"message": "Connection Port",
"description": "Connection port input label"
},
"refreshStatusButton": {
"message": "Refresh Status",
"description": "Refresh status button tooltip"
},
"copyConfigButton": {
"message": "Copy Configuration",
"description": "Copy configuration button text"
},
"retryButton": {
"message": "Retry",
"description": "Retry button text"
},
"cancelButton": {
"message": "Cancel",
"description": "Cancel button text"
},
"confirmButton": {
"message": "Confirm",
"description": "Confirm button text"
},
"saveButton": {
"message": "Save",
"description": "Save button text"
},
"closeButton": {
"message": "Close",
"description": "Close button text"
},
"resetButton": {
"message": "Reset",
"description": "Reset button text"
},
"initializingStatus": {
"message": "Initializing...",
"description": "Initializing progress message"
},
"processingStatus": {
"message": "Processing...",
"description": "Processing progress message"
},
"loadingStatus": {
"message": "Loading...",
"description": "Loading progress message"
},
"clearingStatus": {
"message": "Clearing...",
"description": "Clearing progress message"
},
"cleaningStatus": {
"message": "Cleaning...",
"description": "Cleaning progress message"
},
"downloadingStatus": {
"message": "Downloading...",
"description": "Downloading progress message"
},
"semanticEngineReadyStatus": {
"message": "Semantic Engine Ready",
"description": "Semantic engine ready status"
},
"semanticEngineInitializingStatus": {
"message": "Semantic Engine Initializing...",
"description": "Semantic engine initializing status"
},
"semanticEngineInitFailedStatus": {
"message": "Semantic Engine Initialization Failed",
"description": "Semantic engine initialization failed status"
},
"semanticEngineNotInitStatus": {
"message": "Semantic Engine Not Initialized",
"description": "Semantic engine not initialized status"
},
"initSemanticEngineButton": {
"message": "Initialize Semantic Engine",
"description": "Initialize semantic engine button text"
},
"reinitializeButton": {
"message": "Reinitialize",
"description": "Reinitialize button text"
},
"downloadingModelStatus": {
"message": "Downloading Model... $PROGRESS$%",
"description": "Model download progress with percentage",
"placeholders": {
"progress": {
"content": "$1",
"example": "50"
}
}
},
"switchingModelStatus": {
"message": "Switching Model...",
"description": "Model switching progress message"
},
"modelLoadedStatus": {
"message": "Model Loaded",
"description": "Model successfully loaded status"
},
"modelFailedStatus": {
"message": "Model Failed to Load",
"description": "Model failed to load status"
},
"lightweightModelDescription": {
"message": "Lightweight Multilingual Model",
"description": "Description for lightweight model option"
},
"betterThanSmallDescription": {
"message": "Slightly larger than e5-small, but better performance",
"description": "Description for medium model option"
},
"multilingualModelDescription": {
"message": "Multilingual Semantic Model",
"description": "Description for multilingual model option"
},
"fastPerformance": {
"message": "Fast",
"description": "Fast performance indicator"
},
"balancedPerformance": {
"message": "Balanced",
"description": "Balanced performance indicator"
},
"accuratePerformance": {
"message": "Accurate",
"description": "Accurate performance indicator"
},
"networkErrorMessage": {
"message": "Network connection error, please check network and retry",
"description": "Network connection error message"
},
"modelCorruptedErrorMessage": {
"message": "Model file corrupted or incomplete, please retry download",
"description": "Model corruption error message"
},
"unknownErrorMessage": {
"message": "Unknown error, please check if your network can access HuggingFace",
"description": "Unknown error fallback message"
},
"permissionDeniedErrorMessage": {
"message": "Permission denied",
"description": "Permission denied error message"
},
"timeoutErrorMessage": {
"message": "Operation timed out",
"description": "Timeout error message"
},
"indexedPagesLabel": {
"message": "Indexed Pages",
"description": "Number of indexed pages label"
},
"indexSizeLabel": {
"message": "Index Size",
"description": "Index size label"
},
"activeTabsLabel": {
"message": "Active Tabs",
"description": "Number of active tabs label"
},
"vectorDocumentsLabel": {
"message": "Vector Documents",
"description": "Number of vector documents label"
},
"cacheSizeLabel": {
"message": "Cache Size",
"description": "Cache size label"
},
"cacheEntriesLabel": {
"message": "Cache Entries",
"description": "Number of cache entries label"
},
"clearAllDataButton": {
"message": "Clear All Data",
"description": "Clear all data button text"
},
"clearAllCacheButton": {
"message": "Clear All Cache",
"description": "Clear all cache button text"
},
"cleanExpiredCacheButton": {
"message": "Clean Expired Cache",
"description": "Clean expired cache button text"
},
"exportDataButton": {
"message": "Export Data",
"description": "Export data button text"
},
"importDataButton": {
"message": "Import Data",
"description": "Import data button text"
},
"confirmClearDataTitle": {
"message": "Confirm Clear Data",
"description": "Clear data confirmation dialog title"
},
"settingsTitle": {
"message": "Settings",
"description": "Settings dialog title"
},
"aboutTitle": {
"message": "About",
"description": "About dialog title"
},
"helpTitle": {
"message": "Help",
"description": "Help dialog title"
},
"clearDataWarningMessage": {
"message": "This operation will clear all indexed webpage content and vector data, including:",
"description": "Clear data warning message"
},
"clearDataList1": {
"message": "All webpage text content index",
"description": "First item in clear data list"
},
"clearDataList2": {
"message": "Vector embedding data",
"description": "Second item in clear data list"
},
"clearDataList3": {
"message": "Search history and cache",
"description": "Third item in clear data list"
},
"clearDataIrreversibleWarning": {
"message": "This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.",
"description": "Irreversible operation warning"
},
"confirmClearButton": {
"message": "Confirm Clear",
"description": "Confirm clear action button"
},
"cacheDetailsLabel": {
"message": "Cache Details",
"description": "Cache details section label"
},
"noCacheDataMessage": {
"message": "No cache data",
"description": "No cache data available message"
},
"loadingCacheInfoStatus": {
"message": "Loading cache information...",
"description": "Loading cache information status"
},
"processingCacheStatus": {
"message": "Processing cache...",
"description": "Processing cache status"
},
"expiredLabel": {
"message": "Expired",
"description": "Expired item label"
},
"bookmarksBarLabel": {
"message": "Bookmarks Bar",
"description": "Bookmarks bar folder name"
},
"newTabLabel": {
"message": "New Tab",
"description": "New tab label"
},
"currentPageLabel": {
"message": "Current Page",
"description": "Current page label"
},
"menuLabel": {
"message": "Menu",
"description": "Menu accessibility label"
},
"navigationLabel": {
"message": "Navigation",
"description": "Navigation accessibility label"
},
"mainContentLabel": {
"message": "Main Content",
"description": "Main content accessibility label"
},
"languageSelectorLabel": {
"message": "Language",
"description": "Language selector label"
},
"themeLabel": {
"message": "Theme",
"description": "Theme selector label"
},
"lightTheme": {
"message": "Light",
"description": "Light theme option"
},
"darkTheme": {
"message": "Dark",
"description": "Dark theme option"
},
"autoTheme": {
"message": "Auto",
"description": "Auto theme option"
},
"advancedSettingsLabel": {
"message": "Advanced Settings",
"description": "Advanced settings section label"
},
"debugModeLabel": {
"message": "Debug Mode",
"description": "Debug mode toggle label"
},
"verboseLoggingLabel": {
"message": "Verbose Logging",
"description": "Verbose logging toggle label"
},
"successNotification": {
"message": "Operation completed successfully",
"description": "Generic success notification"
},
"warningNotification": {
"message": "Warning: Please review before proceeding",
"description": "Generic warning notification"
},
"infoNotification": {
"message": "Information",
"description": "Generic info notification"
},
"configCopiedNotification": {
"message": "Configuration copied to clipboard",
"description": "Configuration copied success message"
},
"dataClearedNotification": {
"message": "Data cleared successfully",
"description": "Data cleared success message"
},
"bytesUnit": {
"message": "bytes",
"description": "Bytes unit"
},
"kilobytesUnit": {
"message": "KB",
"description": "Kilobytes unit"
},
"megabytesUnit": {
"message": "MB",
"description": "Megabytes unit"
},
"gigabytesUnit": {
"message": "GB",
"description": "Gigabytes unit"
},
"itemsUnit": {
"message": "items",
"description": "Items count unit"
},
"pagesUnit": {
"message": "pages",
"description": "Pages count unit"
}
}

View File

@@ -0,0 +1,338 @@
{
"extensionName": {
"message": "Chrome MCPサーバー"
},
"extensionDescription": {
"message": "自身のChromeブラウザの機能を外部に公開します"
},
"nativeServerConfigLabel": {
"message": "ネイティブサーバー設定"
},
"semanticEngineLabel": {
"message": "セマンティックエンジン"
},
"embeddingModelLabel": {
"message": "埋め込みモデル"
},
"indexDataManagementLabel": {
"message": "インデックスデータ管理"
},
"modelCacheManagementLabel": {
"message": "モデルキャッシュ管理"
},
"statusLabel": {
"message": "ステータス"
},
"runningStatusLabel": {
"message": "実行ステータス"
},
"connectionStatusLabel": {
"message": "接続ステータス"
},
"lastUpdatedLabel": {
"message": "最終更新:"
},
"connectButton": {
"message": "接続"
},
"disconnectButton": {
"message": "切断"
},
"connectingStatus": {
"message": "接続中..."
},
"connectedStatus": {
"message": "接続済み"
},
"disconnectedStatus": {
"message": "未接続"
},
"detectingStatus": {
"message": "検出中..."
},
"serviceRunningStatus": {
"message": "サービス実行中 (ポート: $1)",
"placeholders": {
"port": {
"content": "$1",
"example": "12306"
}
}
},
"serviceNotConnectedStatus": {
"message": "サービス未接続"
},
"connectedServiceNotStartedStatus": {
"message": "接続済み、サービス未起動"
},
"mcpServerConfigLabel": {
"message": "MCPサーバー設定"
},
"connectionPortLabel": {
"message": "接続ポート"
},
"refreshStatusButton": {
"message": "ステータス更新"
},
"copyConfigButton": {
"message": "設定をコピー"
},
"retryButton": {
"message": "再試行"
},
"cancelButton": {
"message": "キャンセル"
},
"confirmButton": {
"message": "確認"
},
"saveButton": {
"message": "保存"
},
"closeButton": {
"message": "閉じる"
},
"resetButton": {
"message": "リセット"
},
"initializingStatus": {
"message": "初期化中..."
},
"processingStatus": {
"message": "処理中..."
},
"loadingStatus": {
"message": "読み込み中..."
},
"clearingStatus": {
"message": "クリア中..."
},
"cleaningStatus": {
"message": "クリーンアップ中..."
},
"downloadingStatus": {
"message": "ダウンロード中..."
},
"semanticEngineReadyStatus": {
"message": "セマンティックエンジン準備完了"
},
"semanticEngineInitializingStatus": {
"message": "セマンティックエンジン初期化中..."
},
"semanticEngineInitFailedStatus": {
"message": "セマンティックエンジンの初期化に失敗しました"
},
"semanticEngineNotInitStatus": {
"message": "セマンティックエンジン未初期化"
},
"initSemanticEngineButton": {
"message": "セマンティックエンジンを初期化"
},
"reinitializeButton": {
"message": "再初期化"
},
"downloadingModelStatus": {
"message": "モデルをダウンロード中... $1%",
"placeholders": {
"progress": {
"content": "$1",
"example": "50"
}
}
},
"switchingModelStatus": {
"message": "モデルを切り替え中..."
},
"modelLoadedStatus": {
"message": "モデル読み込み完了"
},
"modelFailedStatus": {
"message": "モデルの読み込みに失敗しました"
},
"lightweightModelDescription": {
"message": "軽量多言語モデル"
},
"betterThanSmallDescription": {
"message": "e5-smallよりわずかに大きいが、性能は向上"
},
"multilingualModelDescription": {
"message": "多言語対応セマンティックモデル"
},
"fastPerformance": {
"message": "高速"
},
"balancedPerformance": {
"message": "バランス"
},
"accuratePerformance": {
"message": "高精度"
},
"networkErrorMessage": {
"message": "ネットワーク接続エラーです。ネットワークを確認して再試行してください"
},
"modelCorruptedErrorMessage": {
"message": "モデルファイルが破損しているか不完全です。再ダウンロードしてください"
},
"unknownErrorMessage": {
"message": "不明なエラーです。ネットワークがHuggingFaceにアクセスできるか確認してください"
},
"permissionDeniedErrorMessage": {
"message": "権限が拒否されました"
},
"timeoutErrorMessage": {
"message": "操作がタイムアウトしました"
},
"indexedPagesLabel": {
"message": "インデックス化されたページ"
},
"indexSizeLabel": {
"message": "インデックスサイズ"
},
"activeTabsLabel": {
"message": "アクティブなタブ"
},
"vectorDocumentsLabel": {
"message": "ベクトルドキュメント"
},
"cacheSizeLabel": {
"message": "キャッシュサイズ"
},
"cacheEntriesLabel": {
"message": "キャッシュエントリ"
},
"clearAllDataButton": {
"message": "全データをクリア"
},
"clearAllCacheButton": {
"message": "全キャッシュをクリア"
},
"cleanExpiredCacheButton": {
"message": "期限切れキャッシュをクリーンアップ"
},
"exportDataButton": {
"message": "データのエクスポート"
},
"importDataButton": {
"message": "データのインポート"
},
"confirmClearDataTitle": {
"message": "データクリアの確認"
},
"settingsTitle": {
"message": "設定"
},
"aboutTitle": {
"message": "情報"
},
"helpTitle": {
"message": "ヘルプ"
},
"clearDataWarningMessage": {
"message": "この操作は、インデックス化されたすべてのウェブページコンテンツとベクトルデータをクリアします。これには以下が含まれます:"
},
"clearDataList1": {
"message": "すべてのウェブページテキストコンテンツインデックス"
},
"clearDataList2": {
"message": "ベクトル埋め込みデータ"
},
"clearDataList3": {
"message": "検索履歴とキャッシュ"
},
"clearDataIrreversibleWarning": {
"message": "この操作は元に戻せません!クリア後、再度ウェブページを閲覧してインデックスを再構築する必要があります。"
},
"confirmClearButton": {
"message": "クリアを確認"
},
"cacheDetailsLabel": {
"message": "キャッシュ詳細"
},
"noCacheDataMessage": {
"message": "キャッシュデータがありません"
},
"loadingCacheInfoStatus": {
"message": "キャッシュ情報を読み込み中..."
},
"processingCacheStatus": {
"message": "キャッシュを処理中..."
},
"expiredLabel": {
"message": "期限切れ"
},
"bookmarksBarLabel": {
"message": "ブックマークバー"
},
"newTabLabel": {
"message": "新しいタブ"
},
"currentPageLabel": {
"message": "現在のページ"
},
"menuLabel": {
"message": "メニュー"
},
"navigationLabel": {
"message": "ナビゲーション"
},
"mainContentLabel": {
"message": "メインコンテンツ"
},
"languageSelectorLabel": {
"message": "言語"
},
"themeLabel": {
"message": "テーマ"
},
"lightTheme": {
"message": "ライト"
},
"darkTheme": {
"message": "ダーク"
},
"autoTheme": {
"message": "自動"
},
"advancedSettingsLabel": {
"message": "詳細設定"
},
"debugModeLabel": {
"message": "デバッグモード"
},
"verboseLoggingLabel": {
"message": "詳細ロギング"
},
"successNotification": {
"message": "操作が正常に完了しました"
},
"warningNotification": {
"message": "警告:続行する前に確認してください"
},
"infoNotification": {
"message": "情報"
},
"configCopiedNotification": {
"message": "設定がクリップボードにコピーされました"
},
"dataClearedNotification": {
"message": "データが正常にクリアされました"
},
"bytesUnit": {
"message": "バイト"
},
"kilobytesUnit": {
"message": "KB"
},
"megabytesUnit": {
"message": "MB"
},
"gigabytesUnit": {
"message": "GB"
},
"itemsUnit": {
"message": "項目"
},
"pagesUnit": {
"message": "ページ"
}
}

View File

@@ -0,0 +1,446 @@
{
"extensionName": {
"message": "chrome-mcp-server",
"description": "扩展名称"
},
"extensionDescription": {
"message": "使用你自己的 Chrome 浏览器暴露浏览器功能",
"description": "扩展描述"
},
"nativeServerConfigLabel": {
"message": "Native Server 配置",
"description": "本地服务器设置的主要节标题"
},
"semanticEngineLabel": {
"message": "语义引擎",
"description": "语义引擎的主要节标题"
},
"embeddingModelLabel": {
"message": "Embedding模型",
"description": "模型选择的主要节标题"
},
"indexDataManagementLabel": {
"message": "索引数据管理",
"description": "数据管理的主要节标题"
},
"modelCacheManagementLabel": {
"message": "模型缓存管理",
"description": "缓存管理的主要节标题"
},
"statusLabel": {
"message": "状态",
"description": "通用状态标签"
},
"runningStatusLabel": {
"message": "运行状态",
"description": "服务器运行状态标签"
},
"connectionStatusLabel": {
"message": "连接状态",
"description": "连接状态标签"
},
"lastUpdatedLabel": {
"message": "最后更新:",
"description": "最后更新时间戳标签"
},
"connectButton": {
"message": "连接",
"description": "连接按钮文本"
},
"disconnectButton": {
"message": "断开",
"description": "断开连接按钮文本"
},
"connectingStatus": {
"message": "连接中...",
"description": "连接状态消息"
},
"connectedStatus": {
"message": "已连接",
"description": "已连接状态消息"
},
"disconnectedStatus": {
"message": "已断开",
"description": "已断开状态消息"
},
"detectingStatus": {
"message": "检测中...",
"description": "检测状态消息"
},
"serviceRunningStatus": {
"message": "服务运行中 (端口: $PORT$)",
"description": "带端口号的服务运行状态",
"placeholders": {
"port": {
"content": "$1",
"example": "12306"
}
}
},
"serviceNotConnectedStatus": {
"message": "服务未连接",
"description": "服务未连接状态"
},
"connectedServiceNotStartedStatus": {
"message": "已连接,服务未启动",
"description": "已连接但服务未启动状态"
},
"mcpServerConfigLabel": {
"message": "MCP 服务器配置",
"description": "MCP 服务器配置节标签"
},
"connectionPortLabel": {
"message": "连接端口",
"description": "连接端口输入标签"
},
"refreshStatusButton": {
"message": "刷新状态",
"description": "刷新状态按钮提示"
},
"copyConfigButton": {
"message": "复制配置",
"description": "复制配置按钮文本"
},
"retryButton": {
"message": "重试",
"description": "重试按钮文本"
},
"cancelButton": {
"message": "取消",
"description": "取消按钮文本"
},
"confirmButton": {
"message": "确认",
"description": "确认按钮文本"
},
"saveButton": {
"message": "保存",
"description": "保存按钮文本"
},
"closeButton": {
"message": "关闭",
"description": "关闭按钮文本"
},
"resetButton": {
"message": "重置",
"description": "重置按钮文本"
},
"initializingStatus": {
"message": "初始化中...",
"description": "初始化进度消息"
},
"processingStatus": {
"message": "处理中...",
"description": "处理进度消息"
},
"loadingStatus": {
"message": "加载中...",
"description": "加载进度消息"
},
"clearingStatus": {
"message": "清空中...",
"description": "清空进度消息"
},
"cleaningStatus": {
"message": "清理中...",
"description": "清理进度消息"
},
"downloadingStatus": {
"message": "下载中...",
"description": "下载进度消息"
},
"semanticEngineReadyStatus": {
"message": "语义引擎已就绪",
"description": "语义引擎就绪状态"
},
"semanticEngineInitializingStatus": {
"message": "语义引擎初始化中...",
"description": "语义引擎初始化状态"
},
"semanticEngineInitFailedStatus": {
"message": "语义引擎初始化失败",
"description": "语义引擎初始化失败状态"
},
"semanticEngineNotInitStatus": {
"message": "语义引擎未初始化",
"description": "语义引擎未初始化状态"
},
"initSemanticEngineButton": {
"message": "初始化语义引擎",
"description": "初始化语义引擎按钮文本"
},
"reinitializeButton": {
"message": "重新初始化",
"description": "重新初始化按钮文本"
},
"downloadingModelStatus": {
"message": "下载模型中... $PROGRESS$%",
"description": "带百分比的模型下载进度",
"placeholders": {
"progress": {
"content": "$1",
"example": "50"
}
}
},
"switchingModelStatus": {
"message": "切换模型中...",
"description": "模型切换进度消息"
},
"modelLoadedStatus": {
"message": "模型已加载",
"description": "模型成功加载状态"
},
"modelFailedStatus": {
"message": "模型加载失败",
"description": "模型加载失败状态"
},
"lightweightModelDescription": {
"message": "轻量级多语言模型",
"description": "轻量级模型选项的描述"
},
"betterThanSmallDescription": {
"message": "比e5-small稍大但效果更好",
"description": "中等模型选项的描述"
},
"multilingualModelDescription": {
"message": "多语言语义模型",
"description": "多语言模型选项的描述"
},
"fastPerformance": {
"message": "快速",
"description": "快速性能指示器"
},
"balancedPerformance": {
"message": "平衡",
"description": "平衡性能指示器"
},
"accuratePerformance": {
"message": "精确",
"description": "精确性能指示器"
},
"networkErrorMessage": {
"message": "网络连接错误,请检查网络连接后重试",
"description": "网络连接错误消息"
},
"modelCorruptedErrorMessage": {
"message": "模型文件损坏或不完整,请重试下载",
"description": "模型损坏错误消息"
},
"unknownErrorMessage": {
"message": "未知错误请检查你的网络是否可以访问HuggingFace",
"description": "未知错误回退消息"
},
"permissionDeniedErrorMessage": {
"message": "权限被拒绝",
"description": "权限被拒绝错误消息"
},
"timeoutErrorMessage": {
"message": "操作超时",
"description": "超时错误消息"
},
"indexedPagesLabel": {
"message": "已索引页面",
"description": "已索引页面数量标签"
},
"indexSizeLabel": {
"message": "索引大小",
"description": "索引大小标签"
},
"activeTabsLabel": {
"message": "活跃标签页",
"description": "活跃标签页数量标签"
},
"vectorDocumentsLabel": {
"message": "向量文档",
"description": "向量文档数量标签"
},
"cacheSizeLabel": {
"message": "缓存大小",
"description": "缓存大小标签"
},
"cacheEntriesLabel": {
"message": "缓存条目",
"description": "缓存条目数量标签"
},
"clearAllDataButton": {
"message": "清空所有数据",
"description": "清空所有数据按钮文本"
},
"clearAllCacheButton": {
"message": "清空所有缓存",
"description": "清空所有缓存按钮文本"
},
"cleanExpiredCacheButton": {
"message": "清理过期缓存",
"description": "清理过期缓存按钮文本"
},
"exportDataButton": {
"message": "导出数据",
"description": "导出数据按钮文本"
},
"importDataButton": {
"message": "导入数据",
"description": "导入数据按钮文本"
},
"confirmClearDataTitle": {
"message": "确认清空数据",
"description": "清空数据确认对话框标题"
},
"settingsTitle": {
"message": "设置",
"description": "设置对话框标题"
},
"aboutTitle": {
"message": "关于",
"description": "关于对话框标题"
},
"helpTitle": {
"message": "帮助",
"description": "帮助对话框标题"
},
"clearDataWarningMessage": {
"message": "此操作将清空所有已索引的网页内容和向量数据,包括:",
"description": "清空数据警告消息"
},
"clearDataList1": {
"message": "所有网页的文本内容索引",
"description": "清空数据列表第一项"
},
"clearDataList2": {
"message": "向量嵌入数据",
"description": "清空数据列表第二项"
},
"clearDataList3": {
"message": "搜索历史和缓存",
"description": "清空数据列表第三项"
},
"clearDataIrreversibleWarning": {
"message": "此操作不可撤销!清空后需要重新浏览网页来重建索引。",
"description": "不可逆操作警告"
},
"confirmClearButton": {
"message": "确认清空",
"description": "确认清空操作按钮"
},
"cacheDetailsLabel": {
"message": "缓存详情",
"description": "缓存详情节标签"
},
"noCacheDataMessage": {
"message": "暂无缓存数据",
"description": "无缓存数据可用消息"
},
"loadingCacheInfoStatus": {
"message": "正在加载缓存信息...",
"description": "加载缓存信息状态"
},
"processingCacheStatus": {
"message": "处理缓存中...",
"description": "处理缓存状态"
},
"expiredLabel": {
"message": "已过期",
"description": "过期项标签"
},
"bookmarksBarLabel": {
"message": "书签栏",
"description": "书签栏文件夹名称"
},
"newTabLabel": {
"message": "新标签页",
"description": "新标签页标签"
},
"currentPageLabel": {
"message": "当前页面",
"description": "当前页面标签"
},
"menuLabel": {
"message": "菜单",
"description": "菜单辅助功能标签"
},
"navigationLabel": {
"message": "导航",
"description": "导航辅助功能标签"
},
"mainContentLabel": {
"message": "主要内容",
"description": "主要内容辅助功能标签"
},
"languageSelectorLabel": {
"message": "语言",
"description": "语言选择器标签"
},
"themeLabel": {
"message": "主题",
"description": "主题选择器标签"
},
"lightTheme": {
"message": "浅色",
"description": "浅色主题选项"
},
"darkTheme": {
"message": "深色",
"description": "深色主题选项"
},
"autoTheme": {
"message": "自动",
"description": "自动主题选项"
},
"advancedSettingsLabel": {
"message": "高级设置",
"description": "高级设置节标签"
},
"debugModeLabel": {
"message": "调试模式",
"description": "调试模式切换标签"
},
"verboseLoggingLabel": {
"message": "详细日志",
"description": "详细日志切换标签"
},
"successNotification": {
"message": "操作成功完成",
"description": "通用成功通知"
},
"warningNotification": {
"message": "警告:请在继续之前检查",
"description": "通用警告通知"
},
"infoNotification": {
"message": "信息",
"description": "通用信息通知"
},
"configCopiedNotification": {
"message": "配置已复制到剪贴板",
"description": "配置复制成功消息"
},
"dataClearedNotification": {
"message": "数据清空成功",
"description": "数据清空成功消息"
},
"bytesUnit": {
"message": "字节",
"description": "字节单位"
},
"kilobytesUnit": {
"message": "KB",
"description": "千字节单位"
},
"megabytesUnit": {
"message": "MB",
"description": "兆字节单位"
},
"gigabytesUnit": {
"message": "GB",
"description": "吉字节单位"
},
"itemsUnit": {
"message": "项",
"description": "项目计数单位"
},
"pagesUnit": {
"message": "页",
"description": "页面计数单位"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@@ -0,0 +1,116 @@
/**
* Chrome Extension Constants
* Centralized configuration values and magic constants
*/
// Native Host Configuration
export const NATIVE_HOST = {
NAME: 'com.chromemcp.nativehost',
DEFAULT_PORT: 12306,
} as const;
// Chrome Extension Icons
export const ICONS = {
NOTIFICATION: 'icon/48.png',
} as const;
// Timeouts and Delays (in milliseconds)
export const TIMEOUTS = {
DEFAULT_WAIT: 1000,
NETWORK_CAPTURE_MAX: 30000,
NETWORK_CAPTURE_IDLE: 3000,
SCREENSHOT_DELAY: 100,
KEYBOARD_DELAY: 50,
CLICK_DELAY: 100,
} as const;
// Limits and Thresholds
export const LIMITS = {
MAX_NETWORK_REQUESTS: 100,
MAX_SEARCH_RESULTS: 50,
MAX_BOOKMARK_RESULTS: 100,
MAX_HISTORY_RESULTS: 100,
SIMILARITY_THRESHOLD: 0.1,
VECTOR_DIMENSIONS: 384,
} as const;
// Error Messages
export const ERROR_MESSAGES = {
NATIVE_CONNECTION_FAILED: 'Failed to connect to native host',
NATIVE_DISCONNECTED: 'Native connection disconnected',
SERVER_STATUS_LOAD_FAILED: 'Failed to load server status',
SERVER_STATUS_SAVE_FAILED: 'Failed to save server status',
TOOL_EXECUTION_FAILED: 'Tool execution failed',
INVALID_PARAMETERS: 'Invalid parameters provided',
PERMISSION_DENIED: 'Permission denied',
TAB_NOT_FOUND: 'Tab not found',
ELEMENT_NOT_FOUND: 'Element not found',
NETWORK_ERROR: 'Network error occurred',
} as const;
// Success Messages
export const SUCCESS_MESSAGES = {
TOOL_EXECUTED: 'Tool executed successfully',
CONNECTION_ESTABLISHED: 'Connection established',
SERVER_STARTED: 'Server started successfully',
SERVER_STOPPED: 'Server stopped successfully',
} as const;
// File Extensions and MIME Types
export const FILE_TYPES = {
STATIC_EXTENSIONS: [
'.css',
'.js',
'.png',
'.jpg',
'.jpeg',
'.gif',
'.svg',
'.ico',
'.woff',
'.woff2',
'.ttf',
],
FILTERED_MIME_TYPES: ['text/html', 'text/css', 'text/javascript', 'application/javascript'],
IMAGE_FORMATS: ['png', 'jpeg', 'webp'] as const,
} as const;
// Network Filtering
export const NETWORK_FILTERS = {
EXCLUDED_DOMAINS: [
'google-analytics.com',
'googletagmanager.com',
'facebook.com',
'doubleclick.net',
'googlesyndication.com',
],
STATIC_RESOURCE_TYPES: ['stylesheet', 'image', 'font', 'media', 'other'],
} as const;
// Semantic Similarity Configuration
export const SEMANTIC_CONFIG = {
DEFAULT_MODEL: 'sentence-transformers/all-MiniLM-L6-v2',
CHUNK_SIZE: 512,
CHUNK_OVERLAP: 50,
BATCH_SIZE: 32,
CACHE_SIZE: 1000,
} as const;
// Storage Keys
export const STORAGE_KEYS = {
SERVER_STATUS: 'serverStatus',
SEMANTIC_MODEL: 'selectedModel',
USER_PREFERENCES: 'userPreferences',
VECTOR_INDEX: 'vectorIndex',
} as const;
// Notification Configuration
export const NOTIFICATIONS = {
PRIORITY: 2,
TYPE: 'basic' as const,
} as const;
export enum ExecutionWorld {
ISOLATED = 'ISOLATED',
MAIN = 'MAIN',
}

View File

@@ -0,0 +1,114 @@
/**
* Consolidated message type constants for Chrome extension communication
* Note: Native message types are imported from the shared package
*/
// Message targets for routing
export enum MessageTarget {
Offscreen = 'offscreen',
ContentScript = 'content_script',
Background = 'background',
}
// Background script message types
export const BACKGROUND_MESSAGE_TYPES = {
SWITCH_SEMANTIC_MODEL: 'switch_semantic_model',
GET_MODEL_STATUS: 'get_model_status',
UPDATE_MODEL_STATUS: 'update_model_status',
GET_STORAGE_STATS: 'get_storage_stats',
CLEAR_ALL_DATA: 'clear_all_data',
GET_SERVER_STATUS: 'get_server_status',
REFRESH_SERVER_STATUS: 'refresh_server_status',
SERVER_STATUS_CHANGED: 'server_status_changed',
INITIALIZE_SEMANTIC_ENGINE: 'initialize_semantic_engine',
} as const;
// Offscreen message types
export const OFFSCREEN_MESSAGE_TYPES = {
SIMILARITY_ENGINE_INIT: 'similarityEngineInit',
SIMILARITY_ENGINE_COMPUTE: 'similarityEngineCompute',
SIMILARITY_ENGINE_BATCH_COMPUTE: 'similarityEngineBatchCompute',
SIMILARITY_ENGINE_STATUS: 'similarityEngineStatus',
} as const;
// Content script message types
export const CONTENT_MESSAGE_TYPES = {
WEB_FETCHER_GET_TEXT_CONTENT: 'webFetcherGetTextContent',
WEB_FETCHER_GET_HTML_CONTENT: 'getHtmlContent',
NETWORK_CAPTURE_PING: 'network_capture_ping',
CLICK_HELPER_PING: 'click_helper_ping',
FILL_HELPER_PING: 'fill_helper_ping',
KEYBOARD_HELPER_PING: 'keyboard_helper_ping',
SCREENSHOT_HELPER_PING: 'screenshot_helper_ping',
INTERACTIVE_ELEMENTS_HELPER_PING: 'interactive_elements_helper_ping',
} as const;
// Tool action message types (for chrome.runtime.sendMessage)
export const TOOL_MESSAGE_TYPES = {
// Screenshot related
SCREENSHOT_PREPARE_PAGE_FOR_CAPTURE: 'preparePageForCapture',
SCREENSHOT_GET_PAGE_DETAILS: 'getPageDetails',
SCREENSHOT_GET_ELEMENT_DETAILS: 'getElementDetails',
SCREENSHOT_SCROLL_PAGE: 'scrollPage',
SCREENSHOT_RESET_PAGE_AFTER_CAPTURE: 'resetPageAfterCapture',
// Web content fetching
WEB_FETCHER_GET_HTML_CONTENT: 'getHtmlContent',
WEB_FETCHER_GET_TEXT_CONTENT: 'getTextContent',
// User interactions
CLICK_ELEMENT: 'clickElement',
FILL_ELEMENT: 'fillElement',
SIMULATE_KEYBOARD: 'simulateKeyboard',
// Interactive elements
GET_INTERACTIVE_ELEMENTS: 'getInteractiveElements',
// Network requests
NETWORK_SEND_REQUEST: 'sendPureNetworkRequest',
// Semantic similarity engine
SIMILARITY_ENGINE_INIT: 'similarityEngineInit',
SIMILARITY_ENGINE_COMPUTE_BATCH: 'similarityEngineComputeBatch',
} as const;
// Type unions for type safety
export type BackgroundMessageType =
(typeof BACKGROUND_MESSAGE_TYPES)[keyof typeof BACKGROUND_MESSAGE_TYPES];
export type OffscreenMessageType =
(typeof OFFSCREEN_MESSAGE_TYPES)[keyof typeof OFFSCREEN_MESSAGE_TYPES];
export type ContentMessageType = (typeof CONTENT_MESSAGE_TYPES)[keyof typeof CONTENT_MESSAGE_TYPES];
export type ToolMessageType = (typeof TOOL_MESSAGE_TYPES)[keyof typeof TOOL_MESSAGE_TYPES];
// Legacy enum for backward compatibility (will be deprecated)
export enum SendMessageType {
// Screenshot related message types
ScreenshotPreparePageForCapture = 'preparePageForCapture',
ScreenshotGetPageDetails = 'getPageDetails',
ScreenshotGetElementDetails = 'getElementDetails',
ScreenshotScrollPage = 'scrollPage',
ScreenshotResetPageAfterCapture = 'resetPageAfterCapture',
// Web content fetching related message types
WebFetcherGetHtmlContent = 'getHtmlContent',
WebFetcherGetTextContent = 'getTextContent',
// Click related message types
ClickElement = 'clickElement',
// Input filling related message types
FillElement = 'fillElement',
// Interactive elements related message types
GetInteractiveElements = 'getInteractiveElements',
// Network request capture related message types
NetworkSendRequest = 'sendPureNetworkRequest',
// Keyboard event related message types
SimulateKeyboard = 'simulateKeyboard',
// Semantic similarity engine related message types
SimilarityEngineInit = 'similarityEngineInit',
SimilarityEngineComputeBatch = 'similarityEngineComputeBatch',
}

View File

@@ -0,0 +1,24 @@
import type { CallToolResult, TextContent, ImageContent } from '@modelcontextprotocol/sdk/types.js';
export interface ToolResult extends CallToolResult {
content: (TextContent | ImageContent)[];
isError: boolean;
}
export interface ToolExecutor {
execute(args: any): Promise<ToolResult>;
}
export const createErrorResponse = (
message: string = 'Unknown error, please try again',
): ToolResult => {
return {
content: [
{
type: 'text',
text: message,
},
],
isError: true,
};
};

View File

@@ -0,0 +1,38 @@
import { initNativeHostListener } from './native-host';
import {
initSemanticSimilarityListener,
initializeSemanticEngineIfCached,
} from './semantic-similarity';
import { initStorageManagerListener } from './storage-manager';
import { cleanupModelCache } from '@/utils/semantic-similarity-engine';
/**
* Background script entry point
* Initializes all background services and listeners
*/
export default defineBackground(() => {
// Initialize core services
initNativeHostListener();
initSemanticSimilarityListener();
initStorageManagerListener();
// Conditionally initialize semantic similarity engine if model cache exists
initializeSemanticEngineIfCached()
.then((initialized) => {
if (initialized) {
console.log('Background: Semantic similarity engine initialized from cache');
} else {
console.log(
'Background: Semantic similarity engine initialization skipped (no cache found)',
);
}
})
.catch((error) => {
console.warn('Background: Failed to conditionally initialize semantic engine:', error);
});
// Initial cleanup on startup
cleanupModelCache().catch((error) => {
console.warn('Background: Initial cache cleanup failed:', error);
});
});

View File

@@ -0,0 +1,237 @@
import { NativeMessageType } from 'chrome-mcp-shared';
import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
import {
NATIVE_HOST,
ICONS,
NOTIFICATIONS,
STORAGE_KEYS,
ERROR_MESSAGES,
SUCCESS_MESSAGES,
} from '@/common/constants';
import { handleCallTool } from './tools';
let nativePort: chrome.runtime.Port | null = null;
export const HOST_NAME = NATIVE_HOST.NAME;
/**
* Server status management interface
*/
interface ServerStatus {
isRunning: boolean;
port?: number;
lastUpdated: number;
}
let currentServerStatus: ServerStatus = {
isRunning: false,
lastUpdated: Date.now(),
};
/**
* Save server status to chrome.storage
*/
async function saveServerStatus(status: ServerStatus): Promise<void> {
try {
await chrome.storage.local.set({ [STORAGE_KEYS.SERVER_STATUS]: status });
} catch (error) {
console.error(ERROR_MESSAGES.SERVER_STATUS_SAVE_FAILED, error);
}
}
/**
* Load server status from chrome.storage
*/
async function loadServerStatus(): Promise<ServerStatus> {
try {
const result = await chrome.storage.local.get([STORAGE_KEYS.SERVER_STATUS]);
if (result[STORAGE_KEYS.SERVER_STATUS]) {
return result[STORAGE_KEYS.SERVER_STATUS];
}
} catch (error) {
console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
}
return {
isRunning: false,
lastUpdated: Date.now(),
};
}
/**
* Broadcast server status change to all listeners
*/
function broadcastServerStatusChange(status: ServerStatus): void {
chrome.runtime
.sendMessage({
type: BACKGROUND_MESSAGE_TYPES.SERVER_STATUS_CHANGED,
payload: status,
})
.catch(() => {
// Ignore errors if no listeners are present
});
}
/**
* Connect to the native messaging host
*/
export function connectNativeHost(port: number = NATIVE_HOST.DEFAULT_PORT) {
if (nativePort) {
return;
}
try {
nativePort = chrome.runtime.connectNative(HOST_NAME);
nativePort.onMessage.addListener(async (message) => {
// chrome.notifications.create({
// type: NOTIFICATIONS.TYPE,
// iconUrl: chrome.runtime.getURL(ICONS.NOTIFICATION),
// title: 'Message from native host',
// message: `Received data from host: ${JSON.stringify(message)}`,
// priority: NOTIFICATIONS.PRIORITY,
// });
if (message.type === NativeMessageType.PROCESS_DATA && message.requestId) {
const requestId = message.requestId;
const requestPayload = message.payload;
nativePort?.postMessage({
responseToRequestId: requestId,
payload: {
status: 'success',
message: SUCCESS_MESSAGES.TOOL_EXECUTED,
data: requestPayload,
},
});
} else if (message.type === NativeMessageType.CALL_TOOL && message.requestId) {
const requestId = message.requestId;
try {
const result = await handleCallTool(message.payload);
nativePort?.postMessage({
responseToRequestId: requestId,
payload: {
status: 'success',
message: SUCCESS_MESSAGES.TOOL_EXECUTED,
data: result,
},
});
} catch (error) {
nativePort?.postMessage({
responseToRequestId: requestId,
payload: {
status: 'error',
message: ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
error: error instanceof Error ? error.message : String(error),
},
});
}
} else if (message.type === NativeMessageType.SERVER_STARTED) {
const port = message.payload?.port;
currentServerStatus = {
isRunning: true,
port: port,
lastUpdated: Date.now(),
};
await saveServerStatus(currentServerStatus);
broadcastServerStatusChange(currentServerStatus);
console.log(`${SUCCESS_MESSAGES.SERVER_STARTED} on port ${port}`);
} else if (message.type === NativeMessageType.SERVER_STOPPED) {
currentServerStatus = {
isRunning: false,
port: currentServerStatus.port, // Keep last known port for reconnection
lastUpdated: Date.now(),
};
await saveServerStatus(currentServerStatus);
broadcastServerStatusChange(currentServerStatus);
console.log(SUCCESS_MESSAGES.SERVER_STOPPED);
} else if (message.type === NativeMessageType.ERROR_FROM_NATIVE_HOST) {
console.error('Error from native host:', message.payload?.message || 'Unknown error');
}
});
nativePort.onDisconnect.addListener(() => {
console.error(ERROR_MESSAGES.NATIVE_DISCONNECTED, chrome.runtime.lastError);
nativePort = null;
});
nativePort.postMessage({ type: NativeMessageType.START, payload: { port } });
} catch (error) {
console.error(ERROR_MESSAGES.NATIVE_CONNECTION_FAILED, error);
}
}
/**
* Initialize native host listeners and load initial state
*/
export const initNativeHostListener = () => {
// Initialize server status from storage
loadServerStatus()
.then((status) => {
currentServerStatus = status;
})
.catch((error) => {
console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
});
chrome.runtime.onStartup.addListener(connectNativeHost);
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (
message === NativeMessageType.CONNECT_NATIVE ||
message.type === NativeMessageType.CONNECT_NATIVE
) {
const port =
typeof message === 'object' && message.port ? message.port : NATIVE_HOST.DEFAULT_PORT;
connectNativeHost(port);
sendResponse({ success: true, port });
return true;
}
if (message.type === NativeMessageType.PING_NATIVE) {
const connected = nativePort !== null;
sendResponse({ connected });
return true;
}
if (message.type === NativeMessageType.DISCONNECT_NATIVE) {
if (nativePort) {
nativePort.disconnect();
nativePort = null;
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'No active connection' });
}
return true;
}
if (message.type === BACKGROUND_MESSAGE_TYPES.GET_SERVER_STATUS) {
sendResponse({
success: true,
serverStatus: currentServerStatus,
connected: nativePort !== null,
});
return true;
}
if (message.type === BACKGROUND_MESSAGE_TYPES.REFRESH_SERVER_STATUS) {
loadServerStatus()
.then((storedStatus) => {
currentServerStatus = storedStatus;
sendResponse({
success: true,
serverStatus: currentServerStatus,
connected: nativePort !== null,
});
})
.catch((error) => {
console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
sendResponse({
success: false,
error: ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED,
serverStatus: currentServerStatus,
connected: nativePort !== null,
});
});
return true;
}
});
};

View File

@@ -0,0 +1,373 @@
import type { ModelPreset } from '@/utils/semantic-similarity-engine';
import { OffscreenManager } from '@/utils/offscreen-manager';
import { BACKGROUND_MESSAGE_TYPES, OFFSCREEN_MESSAGE_TYPES } from '@/common/message-types';
import { STORAGE_KEYS, ERROR_MESSAGES } from '@/common/constants';
import { hasAnyModelCache } from '@/utils/semantic-similarity-engine';
/**
* Model configuration state management interface
*/
interface ModelConfig {
modelPreset: ModelPreset;
modelVersion: 'full' | 'quantized' | 'compressed';
modelDimension: number;
}
let currentBackgroundModelConfig: ModelConfig | null = null;
/**
* Initialize semantic engine only if model cache exists
* This is called during plugin startup to avoid downloading models unnecessarily
*/
export async function initializeSemanticEngineIfCached(): Promise<boolean> {
try {
console.log('Background: Checking if semantic engine should be initialized from cache...');
const hasCachedModel = await hasAnyModelCache();
if (!hasCachedModel) {
console.log('Background: No cached models found, skipping semantic engine initialization');
return false;
}
console.log('Background: Found cached models, initializing semantic engine...');
await initializeDefaultSemanticEngine();
return true;
} catch (error) {
console.error('Background: Error during conditional semantic engine initialization:', error);
return false;
}
}
/**
* Initialize default semantic engine model
*/
export async function initializeDefaultSemanticEngine(): Promise<void> {
try {
console.log('Background: Initializing default semantic engine...');
// Update status to initializing
await updateModelStatus('initializing', 0);
const result = await chrome.storage.local.get([STORAGE_KEYS.SEMANTIC_MODEL, 'selectedVersion']);
const defaultModel =
(result[STORAGE_KEYS.SEMANTIC_MODEL] as ModelPreset) || 'multilingual-e5-small';
const defaultVersion =
(result.selectedVersion as 'full' | 'quantized' | 'compressed') || 'quantized';
const { PREDEFINED_MODELS } = await import('@/utils/semantic-similarity-engine');
const modelInfo = PREDEFINED_MODELS[defaultModel];
await OffscreenManager.getInstance().ensureOffscreenDocument();
const response = await chrome.runtime.sendMessage({
target: 'offscreen',
type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
config: {
useLocalFiles: false,
modelPreset: defaultModel,
modelVersion: defaultVersion,
modelDimension: modelInfo.dimension,
forceOffscreen: true,
},
});
if (response && response.success) {
currentBackgroundModelConfig = {
modelPreset: defaultModel,
modelVersion: defaultVersion,
modelDimension: modelInfo.dimension,
};
console.log('Semantic engine initialized successfully:', currentBackgroundModelConfig);
// Update status to ready
await updateModelStatus('ready', 100);
// Also initialize ContentIndexer now that semantic engine is ready
try {
const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
const contentIndexer = getGlobalContentIndexer();
contentIndexer.startSemanticEngineInitialization();
console.log('ContentIndexer initialization triggered after semantic engine initialization');
} catch (indexerError) {
console.warn(
'Failed to initialize ContentIndexer after semantic engine initialization:',
indexerError,
);
}
} else {
const errorMessage = response?.error || ERROR_MESSAGES.TOOL_EXECUTION_FAILED;
await updateModelStatus('error', 0, errorMessage, 'unknown');
throw new Error(errorMessage);
}
} catch (error: any) {
console.error('Background: Failed to initialize default semantic engine:', error);
const errorMessage = error?.message || 'Unknown error during semantic engine initialization';
await updateModelStatus('error', 0, errorMessage, 'unknown');
// Don't throw error, let the extension continue running
}
}
/**
* Check if model switch is needed
*/
function needsModelSwitch(
modelPreset: ModelPreset,
modelVersion: 'full' | 'quantized' | 'compressed',
modelDimension?: number,
): boolean {
if (!currentBackgroundModelConfig) {
return true;
}
const keyFields = ['modelPreset', 'modelVersion', 'modelDimension'];
for (const field of keyFields) {
const newValue =
field === 'modelPreset'
? modelPreset
: field === 'modelVersion'
? modelVersion
: modelDimension;
if (newValue !== currentBackgroundModelConfig[field as keyof ModelConfig]) {
return true;
}
}
return false;
}
/**
* Handle model switching
*/
export async function handleModelSwitch(
modelPreset: ModelPreset,
modelVersion: 'full' | 'quantized' | 'compressed' = 'quantized',
modelDimension?: number,
previousDimension?: number,
): Promise<{ success: boolean; error?: string }> {
try {
const needsSwitch = needsModelSwitch(modelPreset, modelVersion, modelDimension);
if (!needsSwitch) {
await updateModelStatus('ready', 100);
return { success: true };
}
await updateModelStatus('downloading', 0);
try {
await OffscreenManager.getInstance().ensureOffscreenDocument();
} catch (offscreenError) {
console.error('Background: Failed to create offscreen document:', offscreenError);
const errorMessage = `Failed to create offscreen document: ${offscreenError}`;
await updateModelStatus('error', 0, errorMessage, 'unknown');
return { success: false, error: errorMessage };
}
const response = await chrome.runtime.sendMessage({
target: 'offscreen',
type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
config: {
useLocalFiles: false,
modelPreset: modelPreset,
modelVersion: modelVersion,
modelDimension: modelDimension,
forceOffscreen: true,
},
});
if (response && response.success) {
currentBackgroundModelConfig = {
modelPreset: modelPreset,
modelVersion: modelVersion,
modelDimension: modelDimension!,
};
// Only reinitialize ContentIndexer when dimension changes
try {
if (modelDimension && previousDimension && modelDimension !== previousDimension) {
const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
const contentIndexer = getGlobalContentIndexer();
await contentIndexer.reinitialize();
}
} catch (indexerError) {
console.warn('Background: Failed to reinitialize ContentIndexer:', indexerError);
}
await updateModelStatus('ready', 100);
return { success: true };
} else {
const errorMessage = response?.error || 'Failed to switch model';
const errorType = analyzeErrorType(errorMessage);
await updateModelStatus('error', 0, errorMessage, errorType);
throw new Error(errorMessage);
}
} catch (error: any) {
console.error('Model switch failed:', error);
const errorMessage = error.message || 'Unknown error';
const errorType = analyzeErrorType(errorMessage);
await updateModelStatus('error', 0, errorMessage, errorType);
return { success: false, error: errorMessage };
}
}
/**
* Get model status
*/
export async function handleGetModelStatus(): Promise<{
success: boolean;
status?: any;
error?: string;
}> {
try {
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
console.error('Background: chrome.storage.local is not available for status query');
return {
success: true,
status: {
initializationStatus: 'idle',
downloadProgress: 0,
isDownloading: false,
lastUpdated: Date.now(),
},
};
}
const result = await chrome.storage.local.get(['modelState']);
const modelState = result.modelState || {
status: 'idle',
downloadProgress: 0,
isDownloading: false,
lastUpdated: Date.now(),
};
return {
success: true,
status: {
initializationStatus: modelState.status,
downloadProgress: modelState.downloadProgress,
isDownloading: modelState.isDownloading,
lastUpdated: modelState.lastUpdated,
errorMessage: modelState.errorMessage,
errorType: modelState.errorType,
},
};
} catch (error: any) {
console.error('Failed to get model status:', error);
return { success: false, error: error.message };
}
}
/**
* Update model status
*/
export async function updateModelStatus(
status: string,
progress: number,
errorMessage?: string,
errorType?: string,
): Promise<void> {
try {
// Check if chrome.storage is available
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
console.error('Background: chrome.storage.local is not available for status update');
return;
}
const modelState = {
status,
downloadProgress: progress,
isDownloading: status === 'downloading' || status === 'initializing',
lastUpdated: Date.now(),
errorMessage: errorMessage || '',
errorType: errorType || '',
};
await chrome.storage.local.set({ modelState });
} catch (error) {
console.error('Failed to update model status:', error);
}
}
/**
* Handle model status updates from offscreen document
*/
export async function handleUpdateModelStatus(
modelState: any,
): Promise<{ success: boolean; error?: string }> {
try {
// Check if chrome.storage is available
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
console.error('Background: chrome.storage.local is not available');
return { success: false, error: 'chrome.storage.local is not available' };
}
await chrome.storage.local.set({ modelState });
return { success: true };
} catch (error: any) {
console.error('Background: Failed to update model status:', error);
return { success: false, error: error.message };
}
}
/**
* Analyze error type based on error message
*/
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';
}
/**
* Initialize semantic similarity module message listeners
*/
export const initSemanticSimilarityListener = () => {
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === BACKGROUND_MESSAGE_TYPES.SWITCH_SEMANTIC_MODEL) {
handleModelSwitch(
message.modelPreset,
message.modelVersion,
message.modelDimension,
message.previousDimension,
)
.then((result: { success: boolean; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.type === BACKGROUND_MESSAGE_TYPES.GET_MODEL_STATUS) {
handleGetModelStatus()
.then((result: { success: boolean; status?: any; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.type === BACKGROUND_MESSAGE_TYPES.UPDATE_MODEL_STATUS) {
handleUpdateModelStatus(message.modelState)
.then((result: { success: boolean; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.type === BACKGROUND_MESSAGE_TYPES.INITIALIZE_SEMANTIC_ENGINE) {
initializeDefaultSemanticEngine()
.then(() => sendResponse({ success: true }))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
}
});
};

View File

@@ -0,0 +1,112 @@
import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
/**
* Get storage statistics
*/
export async function handleGetStorageStats(): Promise<{
success: boolean;
stats?: any;
error?: string;
}> {
try {
// Get ContentIndexer statistics
const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
const contentIndexer = getGlobalContentIndexer();
// Note: Semantic engine initialization is now user-controlled
// ContentIndexer will be initialized when user manually triggers semantic engine initialization
// Get statistics
const stats = contentIndexer.getStats();
return {
success: true,
stats: {
indexedPages: stats.indexedPages || 0,
totalDocuments: stats.totalDocuments || 0,
totalTabs: stats.totalTabs || 0,
indexSize: stats.indexSize || 0,
isInitialized: stats.isInitialized || false,
semanticEngineReady: stats.semanticEngineReady || false,
semanticEngineInitializing: stats.semanticEngineInitializing || false,
},
};
} catch (error: any) {
console.error('Background: Failed to get storage stats:', error);
return {
success: false,
error: error.message,
stats: {
indexedPages: 0,
totalDocuments: 0,
totalTabs: 0,
indexSize: 0,
isInitialized: false,
semanticEngineReady: false,
semanticEngineInitializing: false,
},
};
}
}
/**
* Clear all data
*/
export async function handleClearAllData(): Promise<{ success: boolean; error?: string }> {
try {
// 1. Clear all ContentIndexer indexes
try {
const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
const contentIndexer = getGlobalContentIndexer();
await contentIndexer.clearAllIndexes();
console.log('Storage: ContentIndexer indexes cleared successfully');
} catch (indexerError) {
console.warn('Background: Failed to clear ContentIndexer indexes:', indexerError);
// Continue with other cleanup operations
}
// 2. Clear all VectorDatabase data
try {
const { clearAllVectorData } = await import('@/utils/vector-database');
await clearAllVectorData();
console.log('Storage: Vector database data cleared successfully');
} catch (vectorError) {
console.warn('Background: Failed to clear vector data:', vectorError);
// Continue with other cleanup operations
}
// 3. Clear related data in chrome.storage (preserve model preferences)
try {
const keysToRemove = ['vectorDatabaseStats', 'lastCleanupTime', 'contentIndexerStats'];
await chrome.storage.local.remove(keysToRemove);
console.log('Storage: Chrome storage data cleared successfully');
} catch (storageError) {
console.warn('Background: Failed to clear chrome storage data:', storageError);
}
return { success: true };
} catch (error: any) {
console.error('Background: Failed to clear all data:', error);
return { success: false, error: error.message };
}
}
/**
* Initialize storage manager module message listeners
*/
export const initStorageManagerListener = () => {
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === BACKGROUND_MESSAGE_TYPES.GET_STORAGE_STATS) {
handleGetStorageStats()
.then((result: { success: boolean; stats?: any; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.type === BACKGROUND_MESSAGE_TYPES.CLEAR_ALL_DATA) {
handleClearAllData()
.then((result: { success: boolean; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
}
});
};

View File

@@ -0,0 +1,95 @@
import { ToolExecutor } from '@/common/tool-handler';
import type { ToolResult } from '@/common/tool-handler';
import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';
const PING_TIMEOUT_MS = 300;
/**
* Base class for browser tool executors
*/
export abstract class BaseBrowserToolExecutor implements ToolExecutor {
abstract name: string;
abstract execute(args: any): Promise<ToolResult>;
/**
* Inject content script into tab
*/
protected async injectContentScript(
tabId: number,
files: string[],
injectImmediately = false,
world: 'MAIN' | 'ISOLATED' = 'ISOLATED',
): Promise<void> {
console.log(`Injecting ${files.join(', ')} into tab ${tabId}`);
// check if script is already injected
try {
const response = await Promise.race([
chrome.tabs.sendMessage(tabId, { action: `${this.name}_ping` }),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error(`${this.name} Ping action to tab ${tabId} timed out`)),
PING_TIMEOUT_MS,
),
),
]);
if (response && response.status === 'pong') {
console.log(
`pong received for action '${this.name}' in tab ${tabId}. Assuming script is active.`,
);
return;
} else {
console.warn(`Unexpected ping response in tab ${tabId}:`, response);
}
} catch (error) {
console.error(
`ping content script failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
try {
await chrome.scripting.executeScript({
target: { tabId },
files,
injectImmediately,
world,
});
console.log(`'${files.join(', ')}' injection successful for tab ${tabId}`);
} catch (injectionError) {
const errorMessage =
injectionError instanceof Error ? injectionError.message : String(injectionError);
console.error(
`Content script '${files.join(', ')}' injection failed for tab ${tabId}: ${errorMessage}`,
);
throw new Error(
`${ERROR_MESSAGES.TOOL_EXECUTION_FAILED}: Failed to inject content script in tab ${tabId}: ${errorMessage}`,
);
}
}
/**
* Send message to tab
*/
protected async sendMessageToTab(tabId: number, message: any): Promise<any> {
try {
const response = await chrome.tabs.sendMessage(tabId, message);
if (response && response.error) {
throw new Error(String(response.error));
}
return response;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(
`Error sending message to tab ${tabId} for action ${message?.action || 'unknown'}: ${errorMessage}`,
);
if (error instanceof Error) {
throw error;
}
throw new Error(errorMessage);
}
}
}

View File

@@ -0,0 +1,602 @@
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();

View File

@@ -0,0 +1,478 @@
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();

View File

@@ -0,0 +1,343 @@
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
const DEBUGGER_PROTOCOL_VERSION = '1.3';
const DEFAULT_MAX_MESSAGES = 100;
interface ConsoleToolParams {
url?: string;
includeExceptions?: boolean;
maxMessages?: number;
}
interface ConsoleMessage {
timestamp: number;
level: string;
text: string;
args?: any[];
source?: string;
url?: string;
lineNumber?: number;
stackTrace?: any;
}
interface ConsoleException {
timestamp: number;
text: string;
url?: string;
lineNumber?: number;
columnNumber?: number;
stackTrace?: any;
}
interface ConsoleResult {
success: boolean;
message: string;
tabId: number;
tabUrl: string;
tabTitle: string;
captureStartTime: number;
captureEndTime: number;
totalDurationMs: number;
messages: ConsoleMessage[];
exceptions: ConsoleException[];
messageCount: number;
exceptionCount: number;
messageLimitReached: boolean;
}
/**
* Tool for capturing console output from browser tabs
*/
class ConsoleTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.CONSOLE;
async execute(args: ConsoleToolParams): Promise<ToolResult> {
const { url, includeExceptions = true, maxMessages = DEFAULT_MAX_MESSAGES } = args;
let targetTab: chrome.tabs.Tab;
try {
if (url) {
// Navigate to the specified URL
targetTab = await this.navigateToUrl(url);
} else {
// Use current active tab
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!activeTab?.id) {
return createErrorResponse('No active tab found and no URL provided.');
}
targetTab = activeTab;
}
if (!targetTab?.id) {
return createErrorResponse('Failed to identify target tab.');
}
const tabId = targetTab.id;
// Capture console messages (one-time capture)
const result = await this.captureConsoleMessages(tabId, {
includeExceptions,
maxMessages,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error: any) {
console.error('ConsoleTool: Critical error during execute:', error);
return createErrorResponse(`Error in ConsoleTool: ${error.message || String(error)}`);
}
}
private async navigateToUrl(url: string): Promise<chrome.tabs.Tab> {
// Check if URL is already open
const existingTabs = await chrome.tabs.query({ url });
if (existingTabs.length > 0 && existingTabs[0]?.id) {
const tab = existingTabs[0];
// Activate the existing tab
await chrome.tabs.update(tab.id!, { active: true });
await chrome.windows.update(tab.windowId, { focused: true });
return tab;
} else {
// Create new tab with the URL
const newTab = await chrome.tabs.create({ url, active: true });
// Wait for tab to be ready
await this.waitForTabReady(newTab.id!);
return newTab;
}
}
private async waitForTabReady(tabId: number): Promise<void> {
return new Promise((resolve) => {
const checkTab = async () => {
try {
const tab = await chrome.tabs.get(tabId);
if (tab.status === 'complete') {
resolve();
} else {
setTimeout(checkTab, 100);
}
} catch (error) {
// Tab might be closed, resolve anyway
resolve();
}
};
checkTab();
});
}
private formatConsoleArgs(args: any[]): string {
if (!args || args.length === 0) return '';
return args
.map((arg) => {
if (arg.type === 'string') {
return arg.value || '';
} else if (arg.type === 'number') {
return String(arg.value || '');
} else if (arg.type === 'boolean') {
return String(arg.value || '');
} else if (arg.type === 'object') {
return arg.description || '[Object]';
} else if (arg.type === 'undefined') {
return 'undefined';
} else if (arg.type === 'function') {
return arg.description || '[Function]';
} else {
return arg.description || arg.value || String(arg);
}
})
.join(' ');
}
private async captureConsoleMessages(
tabId: number,
options: {
includeExceptions: boolean;
maxMessages: number;
},
): Promise<ConsoleResult> {
const { includeExceptions, maxMessages } = options;
const startTime = Date.now();
const messages: ConsoleMessage[] = [];
const exceptions: ConsoleException[] = [];
let limitReached = false;
try {
// Get tab information
const tab = await chrome.tabs.get(tabId);
// Check if debugger is already attached
const targets = await chrome.debugger.getTargets();
const existingTarget = targets.find(
(t) => t.tabId === tabId && t.attached && t.type === 'page',
);
if (existingTarget && !existingTarget.extensionId) {
throw new Error(
`Debugger is already attached to tab ${tabId} by another tool (e.g., DevTools).`,
);
}
// Attach debugger
try {
await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
} catch (error: any) {
if (error.message?.includes('Cannot attach to the target with an attached client')) {
throw new Error(
`Debugger is already attached to tab ${tabId}. This might be DevTools or another extension.`,
);
}
throw error;
}
// Set up event listener to collect messages
const collectedMessages: any[] = [];
const collectedExceptions: any[] = [];
const eventListener = (source: chrome.debugger.Debuggee, method: string, params?: any) => {
if (source.tabId !== tabId) return;
if (method === 'Log.entryAdded' && params?.entry) {
collectedMessages.push(params.entry);
} else if (method === 'Runtime.consoleAPICalled' && params) {
// Convert Runtime.consoleAPICalled to Log.entryAdded format
const logEntry = {
timestamp: params.timestamp,
level: params.type || 'log',
text: this.formatConsoleArgs(params.args || []),
source: 'console-api',
url: params.stackTrace?.callFrames?.[0]?.url,
lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber,
stackTrace: params.stackTrace,
args: params.args,
};
collectedMessages.push(logEntry);
} else if (
method === 'Runtime.exceptionThrown' &&
includeExceptions &&
params?.exceptionDetails
) {
collectedExceptions.push(params.exceptionDetails);
}
};
chrome.debugger.onEvent.addListener(eventListener);
try {
// Enable Runtime domain first to capture console API calls and exceptions
await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable');
// Also enable Log domain to capture other log entries
await chrome.debugger.sendCommand({ tabId }, 'Log.enable');
// Wait for all messages to be flushed
await new Promise((resolve) => setTimeout(resolve, 2000));
// Process collected messages
for (const entry of collectedMessages) {
if (messages.length >= maxMessages) {
limitReached = true;
break;
}
const message: ConsoleMessage = {
timestamp: entry.timestamp,
level: entry.level || 'log',
text: entry.text || '',
source: entry.source,
url: entry.url,
lineNumber: entry.lineNumber,
};
if (entry.stackTrace) {
message.stackTrace = entry.stackTrace;
}
if (entry.args && Array.isArray(entry.args)) {
message.args = entry.args;
}
messages.push(message);
}
// Process collected exceptions
for (const exceptionDetails of collectedExceptions) {
const exception: ConsoleException = {
timestamp: Date.now(),
text:
exceptionDetails.text ||
exceptionDetails.exception?.description ||
'Unknown exception',
url: exceptionDetails.url,
lineNumber: exceptionDetails.lineNumber,
columnNumber: exceptionDetails.columnNumber,
};
if (exceptionDetails.stackTrace) {
exception.stackTrace = exceptionDetails.stackTrace;
}
exceptions.push(exception);
}
} finally {
// Clean up
chrome.debugger.onEvent.removeListener(eventListener);
try {
await chrome.debugger.sendCommand({ tabId }, 'Runtime.disable');
} catch (e) {
console.warn(`ConsoleTool: Error disabling Runtime for tab ${tabId}:`, e);
}
try {
await chrome.debugger.sendCommand({ tabId }, 'Log.disable');
} catch (e) {
console.warn(`ConsoleTool: Error disabling Log for tab ${tabId}:`, e);
}
try {
await chrome.debugger.detach({ tabId });
} catch (e) {
console.warn(`ConsoleTool: Error detaching debugger for tab ${tabId}:`, e);
}
}
const endTime = Date.now();
// Sort messages by timestamp
messages.sort((a, b) => a.timestamp - b.timestamp);
exceptions.sort((a, b) => a.timestamp - b.timestamp);
return {
success: true,
message: `Console capture completed for tab ${tabId}. ${messages.length} messages, ${exceptions.length} exceptions captured.`,
tabId,
tabUrl: tab.url || '',
tabTitle: tab.title || '',
captureStartTime: startTime,
captureEndTime: endTime,
totalDurationMs: endTime - startTime,
messages,
exceptions,
messageCount: messages.length,
exceptionCount: exceptions.length,
messageLimitReached: limitReached,
};
} catch (error: any) {
console.error(`ConsoleTool: Error capturing console messages for tab ${tabId}:`, error);
throw error;
}
}
}
export const consoleTool = new ConsoleTool();

View File

@@ -0,0 +1,232 @@
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import {
parseISO,
subDays,
subWeeks,
subMonths,
subYears,
startOfToday,
startOfYesterday,
isValid,
format,
} from 'date-fns';
interface HistoryToolParams {
text?: string;
startTime?: string;
endTime?: string;
maxResults?: number;
excludeCurrentTabs?: boolean;
}
interface HistoryItem {
id: string;
url?: string;
title?: string;
lastVisitTime?: number; // Timestamp in milliseconds
visitCount?: number;
typedCount?: number;
}
interface HistoryResult {
items: HistoryItem[];
totalCount: number;
timeRange: {
startTime: number;
endTime: number;
startTimeFormatted: string;
endTimeFormatted: string;
};
query?: string;
}
class HistoryTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.HISTORY;
private static readonly ONE_DAY_MS = 24 * 60 * 60 * 1000;
/**
* Parse a date string into milliseconds since epoch.
* Returns null if the date string is invalid.
* Supports:
* - ISO date strings (e.g., "2023-10-31", "2023-10-31T14:30:00.000Z")
* - Relative times: "1 day ago", "2 weeks ago", "3 months ago", "1 year ago"
* - Special keywords: "now", "today", "yesterday"
*/
private parseDateString(dateStr: string | undefined | null): number | null {
if (!dateStr) {
// If an empty or null string is passed, it might mean "no specific date",
// depending on how you want to treat it. Returning null is safer.
return null;
}
const now = new Date();
const lowerDateStr = dateStr.toLowerCase().trim();
if (lowerDateStr === 'now') return now.getTime();
if (lowerDateStr === 'today') return startOfToday().getTime();
if (lowerDateStr === 'yesterday') return startOfYesterday().getTime();
const relativeMatch = lowerDateStr.match(
/^(\d+)\s+(day|days|week|weeks|month|months|year|years)\s+ago$/,
);
if (relativeMatch) {
const amount = parseInt(relativeMatch[1], 10);
const unit = relativeMatch[2];
let resultDate: Date;
if (unit.startsWith('day')) resultDate = subDays(now, amount);
else if (unit.startsWith('week')) resultDate = subWeeks(now, amount);
else if (unit.startsWith('month')) resultDate = subMonths(now, amount);
else if (unit.startsWith('year')) resultDate = subYears(now, amount);
else return null; // Should not happen with the regex
return resultDate.getTime();
}
// Try parsing as ISO or other common date string formats
// Native Date constructor can be unreliable for non-standard formats.
// date-fns' parseISO is good for ISO 8601.
// For other formats, date-fns' parse function is more flexible.
let parsedDate = parseISO(dateStr); // Handles "2023-10-31" or "2023-10-31T10:00:00"
if (isValid(parsedDate)) {
return parsedDate.getTime();
}
// Fallback to new Date() for other potential formats, but with caution
parsedDate = new Date(dateStr);
if (isValid(parsedDate) && dateStr.includes(parsedDate.getFullYear().toString())) {
return parsedDate.getTime();
}
console.warn(`Could not parse date string: ${dateStr}`);
return null;
}
/**
* Format a timestamp as a human-readable date string
*/
private formatDate(timestamp: number): string {
// Using date-fns for consistent and potentially localized formatting
return format(timestamp, 'yyyy-MM-dd HH:mm:ss');
}
async execute(args: HistoryToolParams): Promise<ToolResult> {
try {
console.log('Executing HistoryTool with args:', args);
const {
text = '',
maxResults = 100, // Default to 100 results
excludeCurrentTabs = false,
} = args;
const now = Date.now();
let startTimeMs: number;
let endTimeMs: number;
// Parse startTime
if (args.startTime) {
const parsedStart = this.parseDateString(args.startTime);
if (parsedStart === null) {
return createErrorResponse(
`Invalid format for start time: "${args.startTime}". Supported formats: ISO (YYYY-MM-DD), "today", "yesterday", "X days/weeks/months/years ago".`,
);
}
startTimeMs = parsedStart;
} else {
// Default to 24 hours ago if startTime is not provided
startTimeMs = now - HistoryTool.ONE_DAY_MS;
}
// Parse endTime
if (args.endTime) {
const parsedEnd = this.parseDateString(args.endTime);
if (parsedEnd === null) {
return createErrorResponse(
`Invalid format for end time: "${args.endTime}". Supported formats: ISO (YYYY-MM-DD), "today", "yesterday", "X days/weeks/months/years ago".`,
);
}
endTimeMs = parsedEnd;
} else {
// Default to current time if endTime is not provided
endTimeMs = now;
}
// Validate time range
if (startTimeMs > endTimeMs) {
return createErrorResponse('Start time cannot be after end time.');
}
console.log(
`Searching history from ${this.formatDate(startTimeMs)} to ${this.formatDate(endTimeMs)} for query "${text}"`,
);
const historyItems = await chrome.history.search({
text,
startTime: startTimeMs,
endTime: endTimeMs,
maxResults,
});
console.log(`Found ${historyItems.length} history items before filtering current tabs.`);
let filteredItems = historyItems;
if (excludeCurrentTabs && historyItems.length > 0) {
const currentTabs = await chrome.tabs.query({});
const openUrls = new Set<string>();
currentTabs.forEach((tab) => {
if (tab.url) {
openUrls.add(tab.url);
}
});
if (openUrls.size > 0) {
filteredItems = historyItems.filter((item) => !(item.url && openUrls.has(item.url)));
console.log(
`Filtered out ${historyItems.length - filteredItems.length} items that are currently open. ${filteredItems.length} items remaining.`,
);
}
}
const result: HistoryResult = {
items: filteredItems.map((item) => ({
id: item.id,
url: item.url,
title: item.title,
lastVisitTime: item.lastVisitTime,
visitCount: item.visitCount,
typedCount: item.typedCount,
})),
totalCount: filteredItems.length,
timeRange: {
startTime: startTimeMs,
endTime: endTimeMs,
startTimeFormatted: this.formatDate(startTimeMs),
endTimeFormatted: this.formatDate(endTimeMs),
},
};
if (text) {
result.query = text;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
console.error('Error in HistoryTool.execute:', error);
return createErrorResponse(
`Error retrieving browsing history: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const historyTool = new HistoryTool();

View File

@@ -0,0 +1,14 @@
export { navigateTool, closeTabsTool, goBackOrForwardTool } from './common';
export { windowTool } from './window';
export { vectorSearchTabsContentTool as searchTabsContentTool } from './vector-search';
export { screenshotTool } from './screenshot';
export { webFetcherTool, getInteractiveElementsTool } from './web-fetcher';
export { clickTool, fillTool } from './interaction';
export { networkRequestTool } from './network-request';
export { networkDebuggerStartTool, networkDebuggerStopTool } from './network-capture-debugger';
export { networkCaptureStartTool, networkCaptureStopTool } from './network-capture-web-request';
export { keyboardTool } from './keyboard';
export { historyTool } from './history';
export { bookmarkSearchTool, bookmarkAddTool, bookmarkDeleteTool } from './bookmark';
export { injectScriptTool, sendCommandToInjectScriptTool } from './inject-script';
export { consoleTool } from './console';

View File

@@ -0,0 +1,229 @@
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { ExecutionWorld } from '@/common/constants';
interface InjectScriptParam {
url?: string;
}
interface ScriptConfig {
type: ExecutionWorld;
jsScript: string;
}
interface SendCommandToInjectScriptToolParam {
tabId?: number;
eventName: string;
payload?: string;
}
const injectedTabs = new Map();
class InjectScriptTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.INJECT_SCRIPT;
async execute(args: InjectScriptParam & ScriptConfig): Promise<ToolResult> {
try {
const { url, type, jsScript } = args;
let tab;
if (!type || !jsScript) {
return createErrorResponse('Param [type] and [jsScript] is required');
}
if (url) {
// If URL is provided, check if it's already open
console.log(`Checking if URL is already open: ${url}`);
const allTabs = await chrome.tabs.query({});
// Find tab with matching URL
const matchingTabs = allTabs.filter((t) => {
// Normalize URLs for comparison (remove trailing slashes)
const tabUrl = t.url?.endsWith('/') ? t.url.slice(0, -1) : t.url;
const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
return tabUrl === targetUrl;
});
if (matchingTabs.length > 0) {
// Use existing tab
tab = matchingTabs[0];
console.log(`Found existing tab with URL: ${url}, tab ID: ${tab.id}`);
} else {
// Create new tab with the URL
console.log(`No existing tab found with URL: ${url}, creating new tab`);
tab = await chrome.tabs.create({ url, active: true });
// Wait for page to load
console.log('Waiting for page to load...');
await new Promise((resolve) => setTimeout(resolve, 3000));
}
} else {
// Use active tab
const tabs = await chrome.tabs.query({ active: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
tab = tabs[0];
}
if (!tab.id) {
return createErrorResponse('Tab has no ID');
}
// Make sure tab is active
await chrome.tabs.update(tab.id, { active: true });
const res = await handleInject(tab.id!, { ...args });
return {
content: [
{
type: 'text',
text: JSON.stringify(res),
},
],
isError: false,
};
} catch (error) {
console.error('Error in InjectScriptTool.execute:', error);
return createErrorResponse(
`Inject script error: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
class SendCommandToInjectScriptTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.SEND_COMMAND_TO_INJECT_SCRIPT;
async execute(args: SendCommandToInjectScriptToolParam): Promise<ToolResult> {
try {
const { tabId, eventName, payload } = args;
if (!eventName) {
return createErrorResponse('Param [eventName] is required');
}
if (tabId) {
const tabExists = await isTabExists(tabId);
if (!tabExists) {
return createErrorResponse('The tab:[tabId] is not exists');
}
}
let finalTabId: number | undefined = tabId;
if (finalTabId === undefined) {
// Use active tab
const tabs = await chrome.tabs.query({ active: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
finalTabId = tabs[0].id;
}
if (!finalTabId) {
return createErrorResponse('No active tab found');
}
if (!injectedTabs.has(finalTabId)) {
throw new Error('No script injected in this tab.');
}
const result = await chrome.tabs.sendMessage(finalTabId, {
action: eventName,
payload,
targetWorld: injectedTabs.get(finalTabId).type, // The bridge uses this to decide whether to forward to MAIN world.
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error) {
console.error('Error in InjectScriptTool.execute:', error);
return createErrorResponse(
`Inject script error: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
async function isTabExists(tabId: number) {
try {
await chrome.tabs.get(tabId);
return true;
} catch (error) {
// An error is thrown if the tab doesn't exist.
return false;
}
}
/**
* @description Handles the injection of user scripts into a specific tab.
* @param {number} tabId - The ID of the target tab.
* @param {object} scriptConfig - The configuration object for the script.
*/
async function handleInject(tabId: number, scriptConfig: ScriptConfig) {
if (injectedTabs.has(tabId)) {
// If already injected, run cleanup first to ensure a clean state.
console.log(`Tab ${tabId} already has injections. Cleaning up first.`);
await handleCleanup(tabId);
}
const { type, jsScript } = scriptConfig;
const hasMain = type === ExecutionWorld.MAIN;
if (hasMain) {
// The bridge is essential for MAIN world communication and cleanup.
await chrome.scripting.executeScript({
target: { tabId },
files: ['inject-scripts/inject-bridge.js'],
world: ExecutionWorld.ISOLATED,
});
await chrome.scripting.executeScript({
target: { tabId },
func: (code) => new Function(code)(),
args: [jsScript],
world: ExecutionWorld.MAIN,
});
} else {
await chrome.scripting.executeScript({
target: { tabId },
func: (code) => new Function(code)(),
args: [jsScript],
world: ExecutionWorld.ISOLATED,
});
}
injectedTabs.set(tabId, scriptConfig);
console.log(`Scripts successfully injected into tab ${tabId}.`);
return { injected: true };
}
/**
* @description Triggers the cleanup process in a specific tab.
* @param {number} tabId - The ID of the target tab.
*/
async function handleCleanup(tabId: number) {
if (!injectedTabs.has(tabId)) return;
// Send cleanup signal. The bridge will forward it to the MAIN world.
chrome.tabs
.sendMessage(tabId, { type: 'chrome-mcp:cleanup' })
.catch((err) =>
console.warn(`Could not send cleanup message to tab ${tabId}. It might have been closed.`),
);
injectedTabs.delete(tabId);
console.log(`Cleanup signal sent to tab ${tabId}. State cleared.`);
}
export const injectScriptTool = new InjectScriptTool();
export const sendCommandToInjectScriptTool = new SendCommandToInjectScriptTool();
// --- Automatic Cleanup Listeners ---
chrome.tabs.onRemoved.addListener((tabId) => {
if (injectedTabs.has(tabId)) {
console.log(`Tab ${tabId} closed. Cleaning up state.`);
injectedTabs.delete(tabId);
}
});

View File

@@ -0,0 +1,167 @@
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';
interface Coordinates {
x: number;
y: number;
}
interface ClickToolParams {
selector?: string; // CSS selector for the element to click
coordinates?: Coordinates; // Coordinates to click at (x, y relative to viewport)
waitForNavigation?: boolean; // Whether to wait for navigation to complete after click
timeout?: number; // Timeout in milliseconds for waiting for the element or navigation
}
/**
* Tool for clicking elements on web pages
*/
class ClickTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.CLICK;
/**
* Execute click operation
*/
async execute(args: ClickToolParams): Promise<ToolResult> {
const {
selector,
coordinates,
waitForNavigation = false,
timeout = TIMEOUTS.DEFAULT_WAIT * 5,
} = args;
console.log(`Starting click operation with options:`, args);
if (!selector && !coordinates) {
return createErrorResponse(
ERROR_MESSAGES.INVALID_PARAMETERS + ': Either selector or coordinates must be provided',
);
}
try {
// 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];
if (!tab.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID');
}
await this.injectContentScript(tab.id, ['inject-scripts/click-helper.js']);
// Send click message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.CLICK_ELEMENT,
selector,
coordinates,
waitForNavigation,
timeout,
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message || 'Click operation successful',
elementInfo: result.elementInfo,
navigationOccurred: result.navigationOccurred,
clickMethod: coordinates ? 'coordinates' : 'selector',
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in click operation:', error);
return createErrorResponse(
`Error performing click: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const clickTool = new ClickTool();
interface FillToolParams {
selector: string;
value: string;
}
/**
* Tool for filling form elements on web pages
*/
class FillTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.FILL;
/**
* Execute fill operation
*/
async execute(args: FillToolParams): Promise<ToolResult> {
const { selector, value } = args;
console.log(`Starting fill operation with options:`, args);
if (!selector) {
return createErrorResponse(ERROR_MESSAGES.INVALID_PARAMETERS + ': Selector must be provided');
}
if (value === undefined || value === null) {
return createErrorResponse(ERROR_MESSAGES.INVALID_PARAMETERS + ': Value must be provided');
}
try {
// 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];
if (!tab.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID');
}
await this.injectContentScript(tab.id, ['inject-scripts/fill-helper.js']);
// Send fill message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.FILL_ELEMENT,
selector,
value,
});
if (result.error) {
return createErrorResponse(result.error);
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message || 'Fill operation successful',
elementInfo: result.elementInfo,
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in fill operation:', error);
return createErrorResponse(
`Error filling element: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const fillTool = new FillTool();

View File

@@ -0,0 +1,82 @@
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';
interface KeyboardToolParams {
keys: string; // Required: string representing keys or key combinations to simulate (e.g., "Enter", "Ctrl+C")
selector?: string; // Optional: CSS selector for target element to send keyboard events to
delay?: number; // Optional: delay between keystrokes in milliseconds
}
/**
* Tool for simulating keyboard input on web pages
*/
class KeyboardTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.KEYBOARD;
/**
* Execute keyboard operation
*/
async execute(args: KeyboardToolParams): Promise<ToolResult> {
const { keys, selector, delay = TIMEOUTS.KEYBOARD_DELAY } = args;
console.log(`Starting keyboard operation with options:`, args);
if (!keys) {
return createErrorResponse(
ERROR_MESSAGES.INVALID_PARAMETERS + ': Keys parameter must be provided',
);
}
try {
// 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];
if (!tab.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID');
}
await this.injectContentScript(tab.id, ['inject-scripts/keyboard-helper.js']);
// Send keyboard simulation message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.SIMULATE_KEYBOARD,
keys,
selector,
delay,
});
if (result.error) {
return createErrorResponse(result.error);
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message || 'Keyboard operation successful',
targetElement: result.targetElement,
results: result.results,
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in keyboard operation:', error);
return createErrorResponse(
`Error simulating keyboard events: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const keyboardTool = new KeyboardTool();

View File

@@ -0,0 +1,988 @@
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { LIMITS, NETWORK_FILTERS } from '@/common/constants';
// Static resource file extensions
const STATIC_RESOURCE_EXTENSIONS = [
'.jpg',
'.jpeg',
'.png',
'.gif',
'.svg',
'.webp',
'.ico',
'.bmp', // Images
'.css',
'.scss',
'.less', // Styles
'.js',
'.jsx',
'.ts',
'.tsx', // Scripts
'.woff',
'.woff2',
'.ttf',
'.eot',
'.otf', // Fonts
'.mp3',
'.mp4',
'.avi',
'.mov',
'.wmv',
'.flv',
'.ogg',
'.wav', // Media
'.pdf',
'.doc',
'.docx',
'.xls',
'.xlsx',
'.ppt',
'.pptx', // Documents
];
// Ad and analytics domain list
const AD_ANALYTICS_DOMAINS = NETWORK_FILTERS.EXCLUDED_DOMAINS;
interface NetworkCaptureStartToolParams {
url?: string; // URL to navigate to or focus. If not provided, uses active tab.
maxCaptureTime?: number; // Maximum capture time (milliseconds)
inactivityTimeout?: number; // Inactivity timeout (milliseconds)
includeStatic?: boolean; // Whether to include static resources
}
interface NetworkRequestInfo {
requestId: string;
url: string;
method: string;
type: string;
requestTime: number;
requestHeaders?: Record<string, string>;
requestBody?: string;
responseHeaders?: Record<string, string>;
responseTime?: number;
status?: number;
statusText?: string;
responseSize?: number;
responseType?: string;
responseBody?: string;
errorText?: string;
specificRequestHeaders?: Record<string, string>;
specificResponseHeaders?: Record<string, string>;
mimeType?: string; // Response MIME type
}
interface CaptureInfo {
tabId: number;
tabUrl: string;
tabTitle: string;
startTime: number;
endTime?: number;
requests: Record<string, NetworkRequestInfo>;
maxCaptureTime: number;
inactivityTimeout: number;
includeStatic: boolean;
limitReached?: boolean; // Whether request count limit is reached
}
/**
* Network Capture Start Tool V2 - Uses Chrome webRequest API to start capturing network requests
*/
class NetworkCaptureStartTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.NETWORK_CAPTURE_START;
public static instance: NetworkCaptureStartTool | null = null;
public captureData: Map<number, CaptureInfo> = new Map(); // tabId -> capture data
private captureTimers: Map<number, NodeJS.Timeout> = new Map(); // tabId -> max capture timer
private inactivityTimers: Map<number, NodeJS.Timeout> = new Map(); // tabId -> inactivity timer
private lastActivityTime: Map<number, number> = new Map(); // tabId -> timestamp of last activity
private requestCounters: Map<number, number> = new Map(); // tabId -> count of captured requests
public static MAX_REQUESTS_PER_CAPTURE = LIMITS.MAX_NETWORK_REQUESTS; // Maximum capture request count
private listeners: { [key: string]: (details: any) => void } = {};
// Static resource MIME types list (for filtering)
private static STATIC_MIME_TYPES_TO_FILTER = [
'image/', // All image types
'font/', // All font types
'audio/', // All audio types
'video/', // All video types
'text/css',
'text/javascript',
'application/javascript',
'application/x-javascript',
'application/pdf',
'application/zip',
'application/octet-stream', // Usually for downloads or generic binary data
];
// API response MIME types list (these types are usually not filtered)
private static API_MIME_TYPES = [
'application/json',
'application/xml',
'text/xml',
'application/x-www-form-urlencoded',
'application/graphql',
'application/grpc',
'application/protobuf',
'application/x-protobuf',
'application/x-json',
'application/ld+json',
'application/problem+json',
'application/problem+xml',
'application/soap+xml',
'application/vnd.api+json',
];
constructor() {
super();
if (NetworkCaptureStartTool.instance) {
return NetworkCaptureStartTool.instance;
}
NetworkCaptureStartTool.instance = this;
// Listen for tab close events
chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this));
// Listen for tab creation events
chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this));
}
/**
* Handle tab close events
*/
private handleTabRemoved(tabId: number) {
if (this.captureData.has(tabId)) {
console.log(`NetworkCaptureV2: Tab ${tabId} was closed, cleaning up resources.`);
this.cleanupCapture(tabId);
}
}
/**
* Handle tab creation events
* If a new tab is opened from a tab being captured, automatically start capturing the new tab's requests
*/
private async handleTabCreated(tab: chrome.tabs.Tab) {
try {
// Check if there are any tabs currently capturing
if (this.captureData.size === 0) return;
// Get the openerTabId of the new tab (ID of the tab that opened this tab)
const openerTabId = tab.openerTabId;
if (!openerTabId) return;
// Check if the opener tab is currently capturing
if (!this.captureData.has(openerTabId)) return;
// Get the new tab's ID
const newTabId = tab.id;
if (!newTabId) return;
console.log(
`NetworkCaptureV2: New tab ${newTabId} created from capturing tab ${openerTabId}, will extend capture to it.`,
);
// Get the opener tab's capture settings
const openerCaptureInfo = this.captureData.get(openerTabId);
if (!openerCaptureInfo) return;
// Wait a short time to ensure the tab is ready
await new Promise((resolve) => setTimeout(resolve, 500));
// Start capturing requests for the new tab
await this.startCaptureForTab(newTabId, {
maxCaptureTime: openerCaptureInfo.maxCaptureTime,
inactivityTimeout: openerCaptureInfo.inactivityTimeout,
includeStatic: openerCaptureInfo.includeStatic,
});
console.log(`NetworkCaptureV2: Successfully extended capture to new tab ${newTabId}`);
} catch (error) {
console.error(`NetworkCaptureV2: Error extending capture to new tab:`, error);
}
}
/**
* Determine whether a request should be filtered (based on URL)
*/
private shouldFilterRequest(url: string, includeStatic: boolean): boolean {
try {
const urlObj = new URL(url);
// Check if it's an ad or analytics domain
if (AD_ANALYTICS_DOMAINS.some((domain) => urlObj.hostname.includes(domain))) {
console.log(`NetworkCaptureV2: Filtering ad/analytics domain: ${urlObj.hostname}`);
return true;
}
// If not including static resources, check extensions
if (!includeStatic) {
const path = urlObj.pathname.toLowerCase();
if (STATIC_RESOURCE_EXTENSIONS.some((ext) => path.endsWith(ext))) {
console.log(`NetworkCaptureV2: Filtering static resource by extension: ${path}`);
return true;
}
}
return false;
} catch (e) {
console.error('NetworkCaptureV2: Error filtering URL:', e);
return false;
}
}
/**
* Filter based on MIME type
*/
private shouldFilterByMimeType(mimeType: string, includeStatic: boolean): boolean {
if (!mimeType) return false;
// Always keep API response types
if (NetworkCaptureStartTool.API_MIME_TYPES.some((type) => mimeType.startsWith(type))) {
return false;
}
// If not including static resources, filter out static resource MIME types
if (!includeStatic) {
// Filter static resource MIME types
if (
NetworkCaptureStartTool.STATIC_MIME_TYPES_TO_FILTER.some((type) =>
mimeType.startsWith(type),
)
) {
console.log(`NetworkCaptureV2: Filtering static resource by MIME type: ${mimeType}`);
return true;
}
// Filter all MIME types starting with text/ (except those already in API_MIME_TYPES)
if (mimeType.startsWith('text/')) {
console.log(`NetworkCaptureV2: Filtering text response: ${mimeType}`);
return true;
}
}
return false;
}
/**
* Update last activity time and reset inactivity timer
*/
private updateLastActivityTime(tabId: number): void {
const captureInfo = this.captureData.get(tabId);
if (!captureInfo) return;
this.lastActivityTime.set(tabId, Date.now());
// Reset inactivity timer
if (this.inactivityTimers.has(tabId)) {
clearTimeout(this.inactivityTimers.get(tabId)!);
}
if (captureInfo.inactivityTimeout > 0) {
this.inactivityTimers.set(
tabId,
setTimeout(() => this.checkInactivity(tabId), captureInfo.inactivityTimeout),
);
}
}
/**
* Check for inactivity
*/
private checkInactivity(tabId: number): void {
const captureInfo = this.captureData.get(tabId);
if (!captureInfo) return;
const lastActivity = this.lastActivityTime.get(tabId) || captureInfo.startTime;
const now = Date.now();
const inactiveTime = now - lastActivity;
if (inactiveTime >= captureInfo.inactivityTimeout) {
console.log(
`NetworkCaptureV2: No activity for ${inactiveTime}ms, stopping capture for tab ${tabId}`,
);
this.stopCaptureByInactivity(tabId);
} else {
// If inactivity time hasn't been reached yet, continue checking
const remainingTime = captureInfo.inactivityTimeout - inactiveTime;
this.inactivityTimers.set(
tabId,
setTimeout(() => this.checkInactivity(tabId), remainingTime),
);
}
}
/**
* Stop capture due to inactivity
*/
private async stopCaptureByInactivity(tabId: number): Promise<void> {
const captureInfo = this.captureData.get(tabId);
if (!captureInfo) return;
console.log(`NetworkCaptureV2: Stopping capture due to inactivity for tab ${tabId}`);
await this.stopCapture(tabId);
}
/**
* Clean up capture resources
*/
private cleanupCapture(tabId: number): void {
// Clear timers
if (this.captureTimers.has(tabId)) {
clearTimeout(this.captureTimers.get(tabId)!);
this.captureTimers.delete(tabId);
}
if (this.inactivityTimers.has(tabId)) {
clearTimeout(this.inactivityTimers.get(tabId)!);
this.inactivityTimers.delete(tabId);
}
// Remove data
this.lastActivityTime.delete(tabId);
this.captureData.delete(tabId);
this.requestCounters.delete(tabId);
console.log(`NetworkCaptureV2: Cleaned up all resources for tab ${tabId}`);
}
/**
* Set up request listeners
*/
private setupListeners(): void {
// Before request is sent
this.listeners.onBeforeRequest = (details: chrome.webRequest.WebRequestBodyDetails) => {
const captureInfo = this.captureData.get(details.tabId);
if (!captureInfo) return;
if (this.shouldFilterRequest(details.url, captureInfo.includeStatic)) {
return;
}
const currentCount = this.requestCounters.get(details.tabId) || 0;
if (currentCount >= NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE) {
console.log(
`NetworkCaptureV2: Request limit (${NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE}) reached for tab ${details.tabId}, ignoring new request: ${details.url}`,
);
captureInfo.limitReached = true;
return;
}
this.requestCounters.set(details.tabId, currentCount + 1);
this.updateLastActivityTime(details.tabId);
if (!captureInfo.requests[details.requestId]) {
captureInfo.requests[details.requestId] = {
requestId: details.requestId,
url: details.url,
method: details.method,
type: details.type,
requestTime: details.timeStamp,
};
if (details.requestBody) {
const requestBody = this.processRequestBody(details.requestBody);
if (requestBody) {
captureInfo.requests[details.requestId].requestBody = requestBody;
}
}
console.log(
`NetworkCaptureV2: Captured request ${currentCount + 1}/${NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE} for tab ${details.tabId}: ${details.method} ${details.url}`,
);
}
};
// Send request headers
this.listeners.onSendHeaders = (details: chrome.webRequest.WebRequestHeadersDetails) => {
const captureInfo = this.captureData.get(details.tabId);
if (!captureInfo || !captureInfo.requests[details.requestId]) return;
if (details.requestHeaders) {
const headers: Record<string, string> = {};
details.requestHeaders.forEach((header) => {
headers[header.name] = header.value || '';
});
captureInfo.requests[details.requestId].requestHeaders = headers;
}
};
// Receive response headers
this.listeners.onHeadersReceived = (details: chrome.webRequest.WebResponseHeadersDetails) => {
const captureInfo = this.captureData.get(details.tabId);
if (!captureInfo || !captureInfo.requests[details.requestId]) return;
const requestInfo = captureInfo.requests[details.requestId];
requestInfo.status = details.statusCode;
requestInfo.statusText = details.statusLine;
requestInfo.responseTime = details.timeStamp;
requestInfo.mimeType = details.responseHeaders?.find(
(h) => h.name.toLowerCase() === 'content-type',
)?.value;
// Secondary filtering based on MIME type
if (
requestInfo.mimeType &&
this.shouldFilterByMimeType(requestInfo.mimeType, captureInfo.includeStatic)
) {
delete captureInfo.requests[details.requestId];
const currentCount = this.requestCounters.get(details.tabId) || 0;
if (currentCount > 0) {
this.requestCounters.set(details.tabId, currentCount - 1);
}
console.log(
`NetworkCaptureV2: Filtered request by MIME type (${requestInfo.mimeType}): ${requestInfo.url}`,
);
return;
}
if (details.responseHeaders) {
const headers: Record<string, string> = {};
details.responseHeaders.forEach((header) => {
headers[header.name] = header.value || '';
});
requestInfo.responseHeaders = headers;
}
this.updateLastActivityTime(details.tabId);
};
// Request completed
this.listeners.onCompleted = (details: chrome.webRequest.WebResponseCacheDetails) => {
const captureInfo = this.captureData.get(details.tabId);
if (!captureInfo || !captureInfo.requests[details.requestId]) return;
const requestInfo = captureInfo.requests[details.requestId];
if ('responseSize' in details) {
requestInfo.responseSize = details.fromCache ? 0 : (details as any).responseSize;
}
this.updateLastActivityTime(details.tabId);
};
// Request failed
this.listeners.onErrorOccurred = (details: chrome.webRequest.WebResponseErrorDetails) => {
const captureInfo = this.captureData.get(details.tabId);
if (!captureInfo || !captureInfo.requests[details.requestId]) return;
const requestInfo = captureInfo.requests[details.requestId];
requestInfo.errorText = details.error;
this.updateLastActivityTime(details.tabId);
};
// Register all listeners
chrome.webRequest.onBeforeRequest.addListener(
this.listeners.onBeforeRequest,
{ urls: ['<all_urls>'] },
['requestBody'],
);
chrome.webRequest.onSendHeaders.addListener(
this.listeners.onSendHeaders,
{ urls: ['<all_urls>'] },
['requestHeaders'],
);
chrome.webRequest.onHeadersReceived.addListener(
this.listeners.onHeadersReceived,
{ urls: ['<all_urls>'] },
['responseHeaders'],
);
chrome.webRequest.onCompleted.addListener(this.listeners.onCompleted, { urls: ['<all_urls>'] });
chrome.webRequest.onErrorOccurred.addListener(this.listeners.onErrorOccurred, {
urls: ['<all_urls>'],
});
}
/**
* Remove all request listeners
* Only remove listeners when all tab captures have stopped
*/
private removeListeners(): void {
// Don't remove listeners if there are still tabs being captured
if (this.captureData.size > 0) {
console.log(
`NetworkCaptureV2: Still capturing on ${this.captureData.size} tabs, not removing listeners.`,
);
return;
}
console.log(`NetworkCaptureV2: No more active captures, removing all listeners.`);
if (this.listeners.onBeforeRequest) {
chrome.webRequest.onBeforeRequest.removeListener(this.listeners.onBeforeRequest);
}
if (this.listeners.onSendHeaders) {
chrome.webRequest.onSendHeaders.removeListener(this.listeners.onSendHeaders);
}
if (this.listeners.onHeadersReceived) {
chrome.webRequest.onHeadersReceived.removeListener(this.listeners.onHeadersReceived);
}
if (this.listeners.onCompleted) {
chrome.webRequest.onCompleted.removeListener(this.listeners.onCompleted);
}
if (this.listeners.onErrorOccurred) {
chrome.webRequest.onErrorOccurred.removeListener(this.listeners.onErrorOccurred);
}
// Clear listener object
this.listeners = {};
}
/**
* Process request body data
*/
private processRequestBody(requestBody: chrome.webRequest.WebRequestBody): string | undefined {
if (requestBody.raw && requestBody.raw.length > 0) {
return '[Binary data]';
} else if (requestBody.formData) {
return JSON.stringify(requestBody.formData);
}
return undefined;
}
/**
* Start network request capture for specified tab
* @param tabId Tab ID
* @param options Capture options
*/
private async startCaptureForTab(
tabId: number,
options: {
maxCaptureTime: number;
inactivityTimeout: number;
includeStatic: boolean;
},
): Promise<void> {
const { maxCaptureTime, inactivityTimeout, includeStatic } = options;
// If already capturing, stop first
if (this.captureData.has(tabId)) {
console.log(
`NetworkCaptureV2: Already capturing on tab ${tabId}. Stopping previous session.`,
);
await this.stopCapture(tabId);
}
try {
// Get tab information
const tab = await chrome.tabs.get(tabId);
// Initialize capture data
this.captureData.set(tabId, {
tabId: tabId,
tabUrl: tab.url || '',
tabTitle: tab.title || '',
startTime: Date.now(),
requests: {},
maxCaptureTime,
inactivityTimeout,
includeStatic,
limitReached: false,
});
// Initialize request counter
this.requestCounters.set(tabId, 0);
// Set up listeners
this.setupListeners();
// Update last activity time
this.updateLastActivityTime(tabId);
console.log(
`NetworkCaptureV2: Started capture for tab ${tabId} (${tab.url}). Max requests: ${NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE}, Max time: ${maxCaptureTime}ms, Inactivity: ${inactivityTimeout}ms.`,
);
// Set maximum capture time
if (maxCaptureTime > 0) {
this.captureTimers.set(
tabId,
setTimeout(async () => {
console.log(
`NetworkCaptureV2: Max capture time (${maxCaptureTime}ms) reached for tab ${tabId}.`,
);
await this.stopCapture(tabId);
}, maxCaptureTime),
);
}
} catch (error: any) {
console.error(`NetworkCaptureV2: Error starting capture for tab ${tabId}:`, error);
// Clean up resources
if (this.captureData.has(tabId)) {
this.cleanupCapture(tabId);
}
throw error;
}
}
/**
* Stop capture
* @param tabId Tab ID
*/
public async stopCapture(
tabId: number,
): Promise<{ success: boolean; message?: string; data?: any }> {
const captureInfo = this.captureData.get(tabId);
if (!captureInfo) {
console.log(`NetworkCaptureV2: No capture in progress for tab ${tabId}`);
return { success: false, message: `No capture in progress for tab ${tabId}` };
}
try {
// Record end time
captureInfo.endTime = Date.now();
// Extract common request and response headers
const requestsArray = Object.values(captureInfo.requests);
const commonRequestHeaders = this.analyzeCommonHeaders(requestsArray, 'requestHeaders');
const commonResponseHeaders = this.analyzeCommonHeaders(requestsArray, 'responseHeaders');
// Process request data, remove common headers
const processedRequests = requestsArray.map((req) => {
const finalReq: NetworkRequestInfo = { ...req };
if (finalReq.requestHeaders) {
finalReq.specificRequestHeaders = this.filterOutCommonHeaders(
finalReq.requestHeaders,
commonRequestHeaders,
);
delete finalReq.requestHeaders;
} else {
finalReq.specificRequestHeaders = {};
}
if (finalReq.responseHeaders) {
finalReq.specificResponseHeaders = this.filterOutCommonHeaders(
finalReq.responseHeaders,
commonResponseHeaders,
);
delete finalReq.responseHeaders;
} else {
finalReq.specificResponseHeaders = {};
}
return finalReq;
});
// Sort by time
processedRequests.sort((a, b) => (a.requestTime || 0) - (b.requestTime || 0));
// Remove listeners
this.removeListeners();
// Prepare result data
const resultData = {
captureStartTime: captureInfo.startTime,
captureEndTime: captureInfo.endTime,
totalDurationMs: captureInfo.endTime - captureInfo.startTime,
settingsUsed: {
maxCaptureTime: captureInfo.maxCaptureTime,
inactivityTimeout: captureInfo.inactivityTimeout,
includeStatic: captureInfo.includeStatic,
maxRequests: NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE,
},
commonRequestHeaders,
commonResponseHeaders,
requests: processedRequests,
requestCount: processedRequests.length,
totalRequestsReceived: this.requestCounters.get(tabId) || 0,
requestLimitReached: captureInfo.limitReached || false,
tabUrl: captureInfo.tabUrl,
tabTitle: captureInfo.tabTitle,
};
// Clean up resources
this.cleanupCapture(tabId);
return {
success: true,
data: resultData,
};
} catch (error: any) {
console.error(`NetworkCaptureV2: Error stopping capture for tab ${tabId}:`, error);
// Ensure resources are cleaned up
this.cleanupCapture(tabId);
return {
success: false,
message: `Error stopping capture: ${error.message || String(error)}`,
};
}
}
/**
* Analyze common request or response headers
*/
private analyzeCommonHeaders(
requests: NetworkRequestInfo[],
headerType: 'requestHeaders' | 'responseHeaders',
): Record<string, string> {
if (!requests || requests.length === 0) return {};
// Find headers that are included in all requests
const commonHeaders: Record<string, string> = {};
const firstRequestWithHeaders = requests.find(
(req) => req[headerType] && Object.keys(req[headerType] || {}).length > 0,
);
if (!firstRequestWithHeaders || !firstRequestWithHeaders[headerType]) {
return {};
}
// Get all headers from the first request
const headers = firstRequestWithHeaders[headerType] as Record<string, string>;
const headerNames = Object.keys(headers);
// Check if each header exists in all requests with the same value
for (const name of headerNames) {
const value = headers[name];
const isCommon = requests.every((req) => {
const reqHeaders = req[headerType] as Record<string, string>;
return reqHeaders && reqHeaders[name] === value;
});
if (isCommon) {
commonHeaders[name] = value;
}
}
return commonHeaders;
}
/**
* Filter out common headers
*/
private filterOutCommonHeaders(
headers: Record<string, string>,
commonHeaders: Record<string, string>,
): Record<string, string> {
if (!headers || typeof headers !== 'object') return {};
const specificHeaders: Record<string, string> = {};
// Use Object.keys to avoid ESLint no-prototype-builtins warning
Object.keys(headers).forEach((name) => {
if (!(name in commonHeaders) || headers[name] !== commonHeaders[name]) {
specificHeaders[name] = headers[name];
}
});
return specificHeaders;
}
async execute(args: NetworkCaptureStartToolParams): Promise<ToolResult> {
const {
url: targetUrl,
maxCaptureTime = 3 * 60 * 1000, // Default 3 minutes
inactivityTimeout = 60 * 1000, // Default 1 minute of inactivity before auto-stop
includeStatic = false, // Default: don't include static resources
} = args;
console.log(`NetworkCaptureStartTool: Executing with args:`, args);
try {
// Get current tab or create new tab
let tabToOperateOn: chrome.tabs.Tab;
if (targetUrl) {
// Find tabs matching the URL
const matchingTabs = await chrome.tabs.query({ url: targetUrl });
if (matchingTabs.length > 0) {
// Use existing tab
tabToOperateOn = matchingTabs[0];
console.log(`NetworkCaptureV2: Found existing tab with URL: ${targetUrl}`);
} else {
// Create new tab
console.log(`NetworkCaptureV2: Creating new tab with URL: ${targetUrl}`);
tabToOperateOn = await chrome.tabs.create({ url: targetUrl, active: true });
// Wait for page to load
await new Promise((resolve) => setTimeout(resolve, 1000));
}
} else {
// Use current active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
tabToOperateOn = tabs[0];
}
if (!tabToOperateOn?.id) {
return createErrorResponse('Failed to identify or create a tab');
}
// Use startCaptureForTab method to start capture
try {
await this.startCaptureForTab(tabToOperateOn.id, {
maxCaptureTime,
inactivityTimeout,
includeStatic,
});
} catch (error: any) {
return createErrorResponse(
`Failed to start capture for tab ${tabToOperateOn.id}: ${error.message || String(error)}`,
);
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Network capture V2 started successfully, waiting for stop command.',
tabId: tabToOperateOn.id,
url: tabToOperateOn.url,
maxCaptureTime,
inactivityTimeout,
includeStatic,
maxRequests: NetworkCaptureStartTool.MAX_REQUESTS_PER_CAPTURE,
}),
},
],
isError: false,
};
} catch (error: any) {
console.error('NetworkCaptureStartTool: Critical error:', error);
return createErrorResponse(
`Error in NetworkCaptureStartTool: ${error.message || String(error)}`,
);
}
}
}
/**
* Network capture stop tool V2 - Stop webRequest API capture and return results
*/
class NetworkCaptureStopTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.NETWORK_CAPTURE_STOP;
public static instance: NetworkCaptureStopTool | null = null;
constructor() {
super();
if (NetworkCaptureStopTool.instance) {
return NetworkCaptureStopTool.instance;
}
NetworkCaptureStopTool.instance = this;
}
async execute(): Promise<ToolResult> {
console.log(`NetworkCaptureStopTool: Executing`);
try {
const startTool = NetworkCaptureStartTool.instance;
if (!startTool) {
return createErrorResponse('Network capture V2 start tool instance not found');
}
// Get all tabs currently capturing
const ongoingCaptures = Array.from(startTool.captureData.keys());
console.log(
`NetworkCaptureStopTool: Found ${ongoingCaptures.length} ongoing captures: ${ongoingCaptures.join(', ')}`,
);
if (ongoingCaptures.length === 0) {
return createErrorResponse('No active network captures found in any tab.');
}
// Get current active tab
const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true });
const activeTabId = activeTabs[0]?.id;
// Determine the primary tab to stop
let primaryTabId: number;
if (activeTabId && startTool.captureData.has(activeTabId)) {
// If current active tab is capturing, prioritize stopping it
primaryTabId = activeTabId;
console.log(
`NetworkCaptureStopTool: Active tab ${activeTabId} is capturing, will stop it first.`,
);
} else if (ongoingCaptures.length === 1) {
// If only one tab is capturing, stop it
primaryTabId = ongoingCaptures[0];
console.log(
`NetworkCaptureStopTool: Only one tab ${primaryTabId} is capturing, stopping it.`,
);
} else {
// If multiple tabs are capturing but current active tab is not among them, stop the first one
primaryTabId = ongoingCaptures[0];
console.log(
`NetworkCaptureStopTool: Multiple tabs capturing, active tab not among them. Stopping tab ${primaryTabId} first.`,
);
}
const stopResult = await startTool.stopCapture(primaryTabId);
if (!stopResult.success) {
return createErrorResponse(
stopResult.message || `Failed to stop network capture for tab ${primaryTabId}`,
);
}
// If multiple tabs are capturing, stop other tabs
if (ongoingCaptures.length > 1) {
const otherTabIds = ongoingCaptures.filter((id) => id !== primaryTabId);
console.log(
`NetworkCaptureStopTool: Stopping ${otherTabIds.length} additional captures: ${otherTabIds.join(', ')}`,
);
for (const tabId of otherTabIds) {
try {
await startTool.stopCapture(tabId);
} catch (error) {
console.error(`NetworkCaptureStopTool: Error stopping capture on tab ${tabId}:`, error);
}
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Capture complete. ${stopResult.data?.requestCount || 0} requests captured.`,
tabId: primaryTabId,
tabUrl: stopResult.data?.tabUrl || 'N/A',
tabTitle: stopResult.data?.tabTitle || 'Unknown Tab',
requestCount: stopResult.data?.requestCount || 0,
commonRequestHeaders: stopResult.data?.commonRequestHeaders || {},
commonResponseHeaders: stopResult.data?.commonResponseHeaders || {},
requests: stopResult.data?.requests || [],
captureStartTime: stopResult.data?.captureStartTime,
captureEndTime: stopResult.data?.captureEndTime,
totalDurationMs: stopResult.data?.totalDurationMs,
settingsUsed: stopResult.data?.settingsUsed || {},
totalRequestsReceived: stopResult.data?.totalRequestsReceived || 0,
requestLimitReached: stopResult.data?.requestLimitReached || false,
remainingCaptures: Array.from(startTool.captureData.keys()),
}),
},
],
isError: false,
};
} catch (error: any) {
console.error('NetworkCaptureStopTool: Critical error:', error);
return createErrorResponse(
`Error in NetworkCaptureStopTool: ${error.message || String(error)}`,
);
}
}
}
export const networkCaptureStartTool = new NetworkCaptureStartTool();
export const networkCaptureStopTool = new NetworkCaptureStopTool();

View File

@@ -0,0 +1,80 @@
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';
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 30000; // For sending a single request via content script
interface NetworkRequestToolParams {
url: string; // URL is always required
method?: string; // Defaults to GET
headers?: Record<string, string>; // User-provided headers
body?: any; // User-provided body
timeout?: number; // Timeout for the network request itself
}
/**
* NetworkRequestTool - Sends network requests based on provided parameters.
*/
class NetworkRequestTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.NETWORK_REQUEST;
async execute(args: NetworkRequestToolParams): Promise<ToolResult> {
const {
url,
method = 'GET',
headers = {},
body,
timeout = DEFAULT_NETWORK_REQUEST_TIMEOUT,
} = args;
console.log(`NetworkRequestTool: Executing with options:`, args);
if (!url) {
return createErrorResponse('URL parameter is required.');
}
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
return createErrorResponse('No active tab found or tab has no ID.');
}
const activeTabId = tabs[0].id;
// Ensure content script is available in the target tab
await this.injectContentScript(activeTabId, ['inject-scripts/network-helper.js']);
console.log(
`NetworkRequestTool: Sending to content script: URL=${url}, Method=${method}, Headers=${Object.keys(headers).join(',')}, BodyType=${typeof body}`,
);
const resultFromContentScript = await this.sendMessageToTab(activeTabId, {
action: TOOL_MESSAGE_TYPES.NETWORK_SEND_REQUEST,
url: url,
method: method,
headers: headers,
body: body,
timeout: timeout,
});
console.log(`NetworkRequestTool: Response from content script:`, resultFromContentScript);
return {
content: [
{
type: 'text',
text: JSON.stringify(resultFromContentScript),
},
],
isError: !resultFromContentScript?.success,
};
} catch (error: any) {
console.error('NetworkRequestTool: Error sending network request:', error);
return createErrorResponse(
`Error sending network request: ${error.message || String(error)}`,
);
}
}
}
export const networkRequestTool = new NetworkRequestTool();

View File

@@ -0,0 +1,388 @@
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 const;
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();

View File

@@ -0,0 +1,308 @@
/**
* Vectorized tab content search tool
* Uses vector database for efficient semantic search
*/
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { ContentIndexer } from '@/utils/content-indexer';
import { LIMITS, ERROR_MESSAGES } from '@/common/constants';
import type { SearchResult } from '@/utils/vector-database';
interface VectorSearchResult {
tabId: number;
url: string;
title: string;
semanticScore: number;
matchedSnippet: string;
chunkSource: string;
timestamp: number;
}
/**
* Tool for vectorized search of tab content using semantic similarity
*/
class VectorSearchTabsContentTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.SEARCH_TABS_CONTENT;
private contentIndexer: ContentIndexer;
private isInitialized = false;
constructor() {
super();
this.contentIndexer = new ContentIndexer({
autoIndex: true,
maxChunksPerPage: LIMITS.MAX_SEARCH_RESULTS,
skipDuplicates: true,
});
}
private async initializeIndexer(): Promise<void> {
try {
await this.contentIndexer.initialize();
this.isInitialized = true;
console.log('VectorSearchTabsContentTool: Content indexer initialized successfully');
} catch (error) {
console.error('VectorSearchTabsContentTool: Failed to initialize content indexer:', error);
this.isInitialized = false;
}
}
async execute(args: { query: string }): Promise<ToolResult> {
try {
const { query } = args;
if (!query || query.trim().length === 0) {
return createErrorResponse(
ERROR_MESSAGES.INVALID_PARAMETERS + ': Query parameter is required and cannot be empty',
);
}
console.log(`VectorSearchTabsContentTool: Starting vector search with query: "${query}"`);
// Check semantic engine status
if (!this.contentIndexer.isSemanticEngineReady()) {
if (this.contentIndexer.isSemanticEngineInitializing()) {
return createErrorResponse(
'Vector search engine is still initializing (model downloading). Please wait a moment and try again.',
);
} else {
// Try to initialize
console.log('VectorSearchTabsContentTool: Initializing content indexer...');
await this.initializeIndexer();
// Check semantic engine status again
if (!this.contentIndexer.isSemanticEngineReady()) {
return createErrorResponse('Failed to initialize vector search engine');
}
}
}
// Execute vector search, get more results for deduplication
const searchResults = await this.contentIndexer.searchContent(query, 50);
// Convert search results format
const vectorSearchResults = this.convertSearchResults(searchResults);
// Deduplicate by tab, keep only the highest similarity fragment per tab
const deduplicatedResults = this.deduplicateByTab(vectorSearchResults);
// Sort by similarity and get top 10 results
const topResults = deduplicatedResults
.sort((a, b) => b.semanticScore - a.semanticScore)
.slice(0, 10);
// Get index statistics
const stats = this.contentIndexer.getStats();
const result = {
success: true,
totalTabsSearched: stats.totalTabs,
matchedTabsCount: topResults.length,
vectorSearchEnabled: true,
indexStats: {
totalDocuments: stats.totalDocuments,
totalTabs: stats.totalTabs,
indexedPages: stats.indexedPages,
semanticEngineReady: stats.semanticEngineReady,
semanticEngineInitializing: stats.semanticEngineInitializing,
},
matchedTabs: topResults.map((result) => ({
tabId: result.tabId,
url: result.url,
title: result.title,
semanticScore: result.semanticScore,
matchedSnippets: [result.matchedSnippet],
chunkSource: result.chunkSource,
timestamp: result.timestamp,
})),
};
console.log(
`VectorSearchTabsContentTool: Found ${topResults.length} results with vector search`,
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
console.error('VectorSearchTabsContentTool: Search failed:', error);
return createErrorResponse(
`Vector search failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Ensure all tabs are indexed
*/
private async ensureTabsIndexed(tabs: chrome.tabs.Tab[]): Promise<void> {
const indexPromises = tabs
.filter((tab) => tab.id)
.map(async (tab) => {
try {
await this.contentIndexer.indexTabContent(tab.id!);
} catch (error) {
console.warn(`VectorSearchTabsContentTool: Failed to index tab ${tab.id}:`, error);
}
});
await Promise.allSettled(indexPromises);
}
/**
* Convert search results format
*/
private convertSearchResults(searchResults: SearchResult[]): VectorSearchResult[] {
return searchResults.map((result) => ({
tabId: result.document.tabId,
url: result.document.url,
title: result.document.title,
semanticScore: result.similarity,
matchedSnippet: this.extractSnippet(result.document.chunk.text),
chunkSource: result.document.chunk.source,
timestamp: result.document.timestamp,
}));
}
/**
* Deduplicate by tab, keep only the highest similarity fragment per tab
*/
private deduplicateByTab(results: VectorSearchResult[]): VectorSearchResult[] {
const tabMap = new Map<number, VectorSearchResult>();
for (const result of results) {
const existingResult = tabMap.get(result.tabId);
// If this tab has no result yet, or current result has higher similarity, update it
if (!existingResult || result.semanticScore > existingResult.semanticScore) {
tabMap.set(result.tabId, result);
}
}
return Array.from(tabMap.values());
}
/**
* Extract text snippet for display
*/
private extractSnippet(text: string, maxLength: number = 200): string {
if (text.length <= maxLength) {
return text;
}
// Try to truncate at sentence boundary
const truncated = text.substring(0, maxLength);
const lastSentenceEnd = Math.max(
truncated.lastIndexOf('.'),
truncated.lastIndexOf('!'),
truncated.lastIndexOf('?'),
truncated.lastIndexOf('。'),
truncated.lastIndexOf(''),
truncated.lastIndexOf(''),
);
if (lastSentenceEnd > maxLength * 0.7) {
return truncated.substring(0, lastSentenceEnd + 1);
}
// If no suitable sentence boundary found, truncate at word boundary
const lastSpaceIndex = truncated.lastIndexOf(' ');
if (lastSpaceIndex > maxLength * 0.8) {
return truncated.substring(0, lastSpaceIndex) + '...';
}
return truncated + '...';
}
/**
* Get index statistics
*/
public async getIndexStats() {
if (!this.isInitialized) {
// Don't automatically initialize - just return basic stats
return {
totalDocuments: 0,
totalTabs: 0,
indexSize: 0,
indexedPages: 0,
isInitialized: false,
semanticEngineReady: false,
semanticEngineInitializing: false,
};
}
return this.contentIndexer.getStats();
}
/**
* Manually rebuild index
*/
public async rebuildIndex(): Promise<void> {
if (!this.isInitialized) {
await this.initializeIndexer();
}
try {
// Clear existing indexes
await this.contentIndexer.clearAllIndexes();
// Get all tabs and reindex
const windows = await chrome.windows.getAll({ populate: true });
const allTabs: chrome.tabs.Tab[] = [];
for (const window of windows) {
if (window.tabs) {
allTabs.push(...window.tabs);
}
}
const validTabs = allTabs.filter(
(tab) =>
tab.id &&
tab.url &&
!tab.url.startsWith('chrome://') &&
!tab.url.startsWith('chrome-extension://') &&
!tab.url.startsWith('edge://') &&
!tab.url.startsWith('about:'),
);
await this.ensureTabsIndexed(validTabs);
console.log(`VectorSearchTabsContentTool: Rebuilt index for ${validTabs.length} tabs`);
} catch (error) {
console.error('VectorSearchTabsContentTool: Failed to rebuild index:', error);
throw error;
}
}
/**
* Manually index specified tab
*/
public async indexTab(tabId: number): Promise<void> {
if (!this.isInitialized) {
await this.initializeIndexer();
}
await this.contentIndexer.indexTabContent(tabId);
}
/**
* Remove index for specified tab
*/
public async removeTabIndex(tabId: number): Promise<void> {
if (!this.isInitialized) {
return;
}
await this.contentIndexer.removeTabIndex(tabId);
}
}
// Export tool instance
export const vectorSearchTabsContentTool = new VectorSearchTabsContentTool();

View File

@@ -0,0 +1,229 @@
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';
interface WebFetcherToolParams {
htmlContent?: boolean; // get the visible HTML content of the current page. default: false
textContent?: boolean; // get the visible text content of the current page. default: true
url?: string; // optional URL to fetch content from (if not provided, uses active tab)
selector?: string; // optional CSS selector to get content from a specific element
}
class WebFetcherTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.WEB_FETCHER;
/**
* Execute web fetcher operation
*/
async execute(args: WebFetcherToolParams): Promise<ToolResult> {
// Handle mutually exclusive parameters: if htmlContent is true, textContent is forced to false
const htmlContent = args.htmlContent === true;
const textContent = htmlContent ? false : args.textContent !== false; // Default is true, unless htmlContent is true or textContent is explicitly set to false
const url = args.url;
const selector = args.selector;
console.log(`Starting web fetcher with options:`, {
htmlContent,
textContent,
url,
selector,
});
try {
// Get tab to fetch content from
let tab;
if (url) {
// If URL is provided, check if it's already open
console.log(`Checking if URL is already open: ${url}`);
const allTabs = await chrome.tabs.query({});
// Find tab with matching URL
const matchingTabs = allTabs.filter((t) => {
// Normalize URLs for comparison (remove trailing slashes)
const tabUrl = t.url?.endsWith('/') ? t.url.slice(0, -1) : t.url;
const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
return tabUrl === targetUrl;
});
if (matchingTabs.length > 0) {
// Use existing tab
tab = matchingTabs[0];
console.log(`Found existing tab with URL: ${url}, tab ID: ${tab.id}`);
} else {
// Create new tab with the URL
console.log(`No existing tab found with URL: ${url}, creating new tab`);
tab = await chrome.tabs.create({ url, active: true });
// Wait for page to load
console.log('Waiting for page to load...');
await new Promise((resolve) => setTimeout(resolve, 3000));
}
} else {
// Use active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
tab = tabs[0];
}
if (!tab.id) {
return createErrorResponse('Tab has no ID');
}
// Make sure tab is active
await chrome.tabs.update(tab.id, { active: true });
// Prepare result object
const result: any = {
success: true,
url: tab.url,
title: tab.title,
};
await this.injectContentScript(tab.id, ['inject-scripts/web-fetcher-helper.js']);
// Get HTML content if requested
if (htmlContent) {
const htmlResponse = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_HTML_CONTENT,
selector: selector,
});
if (htmlResponse.success) {
result.htmlContent = htmlResponse.htmlContent;
} else {
console.error('Failed to get HTML content:', htmlResponse.error);
result.htmlContentError = htmlResponse.error;
}
}
// Get text content if requested (and htmlContent is not true)
if (textContent) {
const textResponse = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_TEXT_CONTENT,
selector: selector,
});
if (textResponse.success) {
result.textContent = textResponse.textContent;
// Include article metadata if available
if (textResponse.article) {
result.article = {
title: textResponse.article.title,
byline: textResponse.article.byline,
siteName: textResponse.article.siteName,
excerpt: textResponse.article.excerpt,
lang: textResponse.article.lang,
};
}
// Include page metadata if available
if (textResponse.metadata) {
result.metadata = textResponse.metadata;
}
} else {
console.error('Failed to get text content:', textResponse.error);
result.textContentError = textResponse.error;
}
}
// Interactive elements feature has been removed
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error) {
console.error('Error in web fetcher:', error);
return createErrorResponse(
`Error fetching web content: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const webFetcherTool = new WebFetcherTool();
interface GetInteractiveElementsToolParams {
textQuery?: string; // Text to search for within interactive elements (fuzzy search)
selector?: string; // CSS selector to filter interactive elements
includeCoordinates?: boolean; // Include element coordinates in the response (default: true)
types?: string[]; // Types of interactive elements to include (default: all types)
}
class GetInteractiveElementsTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.GET_INTERACTIVE_ELEMENTS;
/**
* Execute get interactive elements operation
*/
async execute(args: GetInteractiveElementsToolParams): Promise<ToolResult> {
const { textQuery, selector, includeCoordinates = true, types } = args;
console.log(`Starting get interactive elements with options:`, args);
try {
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
const tab = tabs[0];
if (!tab.id) {
return createErrorResponse('Active tab has no ID');
}
// Ensure content script is injected
await this.injectContentScript(tab.id, ['inject-scripts/interactive-elements-helper.js']);
// Send message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.GET_INTERACTIVE_ELEMENTS,
textQuery,
selector,
includeCoordinates,
types,
});
if (!result.success) {
return createErrorResponse(result.error || 'Failed to get interactive elements');
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
elements: result.elements,
count: result.elements.length,
query: {
textQuery,
selector,
types: types || 'all',
},
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in get interactive elements operation:', error);
return createErrorResponse(
`Error getting interactive elements: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const getInteractiveElementsTool = new GetInteractiveElementsTool();

View File

@@ -0,0 +1,54 @@
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
class WindowTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.GET_WINDOWS_AND_TABS;
async execute(): Promise<ToolResult> {
try {
const windows = await chrome.windows.getAll({ populate: true });
let tabCount = 0;
const structuredWindows = windows.map((window) => {
const tabs =
window.tabs?.map((tab) => {
tabCount++;
return {
tabId: tab.id || 0,
url: tab.url || '',
title: tab.title || '',
active: tab.active || false,
};
}) || [];
return {
windowId: window.id || 0,
tabs: tabs,
};
});
const result = {
windowCount: windows.length,
tabCount: tabCount,
windows: structuredWindows,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error) {
console.error('Error in WindowTool.execute:', error);
return createErrorResponse(
`Error getting windows and tabs information: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const windowTool = new WindowTool();

View File

@@ -0,0 +1,33 @@
import { createErrorResponse } from '@/common/tool-handler';
import { ERROR_MESSAGES } from '@/common/constants';
import * as browserTools from './browser';
const tools = { ...browserTools };
const toolsMap = new Map(Object.values(tools).map((tool) => [tool.name, tool]));
/**
* Tool call parameter interface
*/
export interface ToolCallParam {
name: string;
args: any;
}
/**
* Handle tool execution
*/
export const handleCallTool = async (param: ToolCallParam) => {
const tool = toolsMap.get(param.name);
if (!tool) {
return createErrorResponse(`Tool ${param.name} not found`);
}
try {
return await tool.execute(param.args);
} catch (error) {
console.error(`Tool execution failed for ${param.name}:`, error);
return createErrorResponse(
error instanceof Error ? error.message : ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
);
}
};

View File

@@ -0,0 +1,4 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {},
});

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,287 @@
<template>
<div v-if="visible" class="confirmation-dialog" @click.self="$emit('cancel')">
<div class="dialog-content">
<div class="dialog-header">
<span class="dialog-icon">{{ icon }}</span>
<h3 class="dialog-title">{{ title }}</h3>
</div>
<div class="dialog-body">
<p class="dialog-message">{{ message }}</p>
<ul v-if="items && items.length > 0" class="dialog-list">
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
<div v-if="warning" class="dialog-warning">
<strong>{{ warning }}</strong>
</div>
</div>
<div class="dialog-actions">
<button class="dialog-button cancel-button" @click="$emit('cancel')">
{{ cancelText }}
</button>
<button
class="dialog-button confirm-button"
:disabled="isConfirming"
@click="$emit('confirm')"
>
{{ isConfirming ? confirmingText : confirmText }}
</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getMessage } from '@/utils/i18n';
interface Props {
visible: boolean;
title: string;
message: string;
items?: string[];
warning?: string;
icon?: string;
confirmText?: string;
cancelText?: string;
confirmingText?: string;
isConfirming?: boolean;
}
interface Emits {
(e: 'confirm'): void;
(e: 'cancel'): void;
}
withDefaults(defineProps<Props>(), {
icon: '⚠️',
confirmText: getMessage('confirmButton'),
cancelText: getMessage('cancelButton'),
confirmingText: getMessage('processingStatus'),
isConfirming: false,
});
defineEmits<Emits>();
</script>
<style scoped>
.confirmation-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(8px);
animation: dialogFadeIn 0.3s ease-out;
}
@keyframes dialogFadeIn {
from {
opacity: 0;
backdrop-filter: blur(0px);
}
to {
opacity: 1;
backdrop-filter: blur(8px);
}
}
.dialog-content {
background: white;
border-radius: 12px;
padding: 24px;
max-width: 360px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: dialogSlideIn 0.3s ease-out;
border: 1px solid rgba(255, 255, 255, 0.2);
}
@keyframes dialogSlideIn {
from {
opacity: 0;
transform: translateY(-30px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.dialog-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.dialog-icon {
font-size: 24px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.dialog-title {
font-size: 18px;
font-weight: 600;
color: #2d3748;
margin: 0;
}
.dialog-body {
margin-bottom: 24px;
}
.dialog-message {
font-size: 14px;
color: #4a5568;
margin: 0 0 16px 0;
line-height: 1.6;
}
.dialog-list {
margin: 16px 0;
padding-left: 24px;
background: linear-gradient(135deg, #f7fafc, #edf2f7);
border-radius: 6px;
padding: 12px 12px 12px 32px;
border-left: 3px solid #667eea;
}
.dialog-list li {
font-size: 13px;
color: #718096;
margin-bottom: 6px;
line-height: 1.4;
}
.dialog-list li:last-child {
margin-bottom: 0;
}
.dialog-warning {
font-size: 13px;
color: #e53e3e;
margin: 16px 0 0 0;
padding: 12px;
background: linear-gradient(135deg, rgba(245, 101, 101, 0.1), rgba(229, 62, 62, 0.05));
border-radius: 6px;
border-left: 3px solid #e53e3e;
border: 1px solid rgba(245, 101, 101, 0.2);
}
.dialog-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.dialog-button {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
min-width: 80px;
}
.cancel-button {
background: linear-gradient(135deg, #e2e8f0, #cbd5e0);
color: #4a5568;
border: 1px solid #cbd5e0;
}
.cancel-button:hover {
background: linear-gradient(135deg, #cbd5e0, #a0aec0);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(160, 174, 192, 0.3);
}
.confirm-button {
background: linear-gradient(135deg, #f56565, #e53e3e);
color: white;
border: 1px solid #e53e3e;
}
.confirm-button:hover:not(:disabled) {
background: linear-gradient(135deg, #e53e3e, #c53030);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
}
.confirm-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 响应式设计 */
@media (max-width: 420px) {
.dialog-content {
padding: 20px;
max-width: 320px;
}
.dialog-header {
gap: 10px;
margin-bottom: 16px;
}
.dialog-icon {
font-size: 20px;
}
.dialog-title {
font-size: 16px;
}
.dialog-message {
font-size: 13px;
}
.dialog-list {
padding: 10px 10px 10px 28px;
}
.dialog-list li {
font-size: 12px;
}
.dialog-warning {
font-size: 12px;
padding: 10px;
}
.dialog-actions {
gap: 8px;
flex-direction: column-reverse;
}
.dialog-button {
width: 100%;
padding: 12px 16px;
}
}
/* 焦点样式 */
.dialog-button:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3);
}
.cancel-button:focus {
box-shadow: 0 0 0 3px rgba(160, 174, 192, 0.3);
}
.confirm-button:focus {
box-shadow: 0 0 0 3px rgba(245, 101, 101, 0.3);
}
</style>

View File

@@ -0,0 +1,320 @@
<template>
<div class="model-cache-section">
<h2 class="section-title">{{ getMessage('modelCacheManagementLabel') }}</h2>
<!-- Cache Statistics Grid -->
<div class="stats-grid">
<div class="stats-card">
<div class="stats-header">
<p class="stats-label">{{ getMessage('cacheSizeLabel') }}</p>
<span class="stats-icon orange">
<DatabaseIcon />
</span>
</div>
<p class="stats-value">{{ cacheStats?.totalSizeMB || 0 }} MB</p>
</div>
<div class="stats-card">
<div class="stats-header">
<p class="stats-label">{{ getMessage('cacheEntriesLabel') }}</p>
<span class="stats-icon purple">
<VectorIcon />
</span>
</div>
<p class="stats-value">{{ cacheStats?.entryCount || 0 }}</p>
</div>
</div>
<!-- Cache Entries Details -->
<div v-if="cacheStats && cacheStats.entries.length > 0" class="cache-details">
<h3 class="cache-details-title">{{ getMessage('cacheDetailsLabel') }}</h3>
<div class="cache-entries">
<div v-for="entry in cacheStats.entries" :key="entry.url" class="cache-entry">
<div class="entry-info">
<div class="entry-url">{{ getModelNameFromUrl(entry.url) }}</div>
<div class="entry-details">
<span class="entry-size">{{ entry.sizeMB }} MB</span>
<span class="entry-age">{{ entry.age }}</span>
<span v-if="entry.expired" class="entry-expired">{{ getMessage('expiredLabel') }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- No Cache Message -->
<div v-else-if="cacheStats && cacheStats.entries.length === 0" class="no-cache">
<p>{{ getMessage('noCacheDataMessage') }}</p>
</div>
<!-- Loading State -->
<div v-else-if="!cacheStats" class="loading-cache">
<p>{{ getMessage('loadingCacheInfoStatus') }}</p>
</div>
<!-- Progress Indicator -->
<ProgressIndicator
v-if="isManagingCache"
:visible="isManagingCache"
:text="isManagingCache ? getMessage('processingCacheStatus') : ''"
:showSpinner="true"
/>
<!-- Action Buttons -->
<div class="cache-actions">
<div class="secondary-button" :disabled="isManagingCache" @click="$emit('cleanup-cache')">
<span class="stats-icon"><DatabaseIcon /></span>
<span>{{
isManagingCache ? getMessage('cleaningStatus') : getMessage('cleanExpiredCacheButton')
}}</span>
</div>
<div class="danger-button" :disabled="isManagingCache" @click="$emit('clear-all-cache')">
<span class="stats-icon"><TrashIcon /></span>
<span>{{ isManagingCache ? getMessage('clearingStatus') : getMessage('clearAllCacheButton') }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import ProgressIndicator from './ProgressIndicator.vue';
import { DatabaseIcon, VectorIcon, TrashIcon } from './icons';
import { getMessage } from '@/utils/i18n';
interface CacheEntry {
url: string;
size: number;
sizeMB: number;
timestamp: number;
age: string;
expired: boolean;
}
interface CacheStats {
totalSize: number;
totalSizeMB: number;
entryCount: number;
entries: CacheEntry[];
}
interface Props {
cacheStats: CacheStats | null;
isManagingCache: boolean;
}
interface Emits {
(e: 'cleanup-cache'): void;
(e: 'clear-all-cache'): void;
}
defineProps<Props>();
defineEmits<Emits>();
const getModelNameFromUrl = (url: string) => {
// Extract model name from HuggingFace URL
const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)/);
if (match) {
return match[1];
}
return url.split('/').pop() || url;
};
</script>
<style scoped>
.model-cache-section {
margin-bottom: 24px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.stats-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 16px;
}
.stats-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.stats-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
}
.stats-icon {
padding: 8px;
border-radius: 8px;
width: 36px;
height: 36px;
}
.stats-icon.orange {
background: #fed7aa;
color: #ea580c;
}
.stats-icon.purple {
background: #e9d5ff;
color: #9333ea;
}
.stats-value {
font-size: 30px;
font-weight: 700;
color: #0f172a;
margin: 0;
}
.cache-details {
margin-bottom: 16px;
}
.cache-details-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 0 0 12px 0;
}
.cache-entries {
display: flex;
flex-direction: column;
gap: 8px;
}
.cache-entry {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
}
.entry-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.entry-url {
font-weight: 500;
color: #1f2937;
font-size: 14px;
}
.entry-details {
display: flex;
gap: 8px;
align-items: center;
font-size: 12px;
}
.entry-size {
background: #dbeafe;
color: #1e40af;
padding: 2px 6px;
border-radius: 4px;
}
.entry-age {
color: #6b7280;
}
.entry-expired {
background: #fee2e2;
color: #dc2626;
padding: 2px 6px;
border-radius: 4px;
}
.no-cache,
.loading-cache {
text-align: center;
color: #6b7280;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
margin-bottom: 16px;
}
.cache-actions {
display: flex;
flex-direction: column;
gap: 12px;
}
.secondary-button {
background: #f1f5f9;
color: #475569;
border: 1px solid #cbd5e1;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
justify-content: center;
user-select: none;
cursor: pointer;
}
.secondary-button:hover:not(:disabled) {
background: #e2e8f0;
border-color: #94a3b8;
}
.secondary-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.danger-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: white;
border: 1px solid #d1d5db;
color: #374151;
font-weight: 600;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
}
.danger-button:hover:not(:disabled) {
border-color: #ef4444;
color: #dc2626;
}
.danger-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div v-if="visible" class="progress-section">
<div class="progress-indicator">
<div class="spinner" v-if="showSpinner"></div>
<span class="progress-text">{{ text }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
interface Props {
visible?: boolean;
text: string;
showSpinner?: boolean;
}
withDefaults(defineProps<Props>(), {
visible: true,
showSpinner: true,
});
</script>
<style scoped>
.progress-section {
margin-top: 16px;
animation: slideIn 0.3s ease-out;
}
.progress-indicator {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border-radius: 8px;
border-left: 4px solid #667eea;
backdrop-filter: blur(10px);
border: 1px solid rgba(102, 126, 234, 0.2);
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(102, 126, 234, 0.2);
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.progress-text {
font-size: 14px;
color: #4a5568;
font-weight: 500;
line-height: 1.4;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 420px) {
.progress-indicator {
padding: 12px;
gap: 8px;
}
.spinner {
width: 16px;
height: 16px;
border-width: 2px;
}
.progress-text {
font-size: 13px;
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>

View File

@@ -0,0 +1,24 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
:class="className"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.052-.143Z"
clip-rule="evenodd"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-small',
});
</script>

View File

@@ -0,0 +1,26 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>

View File

@@ -0,0 +1,26 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>

View File

@@ -0,0 +1,26 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-16.5 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 0 0 2.25-2.25V6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>

View File

@@ -0,0 +1,26 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>

View File

@@ -0,0 +1,26 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 4.5a4.5 4.5 0 0 1 6 0M9 4.5V3a1.5 1.5 0 0 1 1.5-1.5h3A1.5 1.5 0 0 1 15 3v1.5M9 4.5a4.5 4.5 0 0 0-4.5 4.5v7.5A1.5 1.5 0 0 0 6 18h12a1.5 1.5 0 0 0 1.5-1.5V9a4.5 4.5 0 0 0-4.5-4.5M12 12l2.25 2.25M12 12l-2.25-2.25M12 12v6"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>

View File

@@ -0,0 +1,7 @@
export { default as DocumentIcon } from './DocumentIcon.vue';
export { default as DatabaseIcon } from './DatabaseIcon.vue';
export { default as BoltIcon } from './BoltIcon.vue';
export { default as TrashIcon } from './TrashIcon.vue';
export { default as CheckIcon } from './CheckIcon.vue';
export { default as TabIcon } from './TabIcon.vue';
export { default as VectorIcon } from './VectorIcon.vue';

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Default Popup Title</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';
createApp(App).mount('#app');

View File

@@ -0,0 +1,246 @@
/* 现代化全局样式 */
:root {
/* 字体系统 */
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
font-weight: 400;
/* 颜色系统 */
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--primary-color: #667eea;
--primary-dark: #5a67d8;
--secondary-color: #764ba2;
--success-color: #48bb78;
--warning-color: #ed8936;
--error-color: #f56565;
--info-color: #4299e1;
--text-primary: #2d3748;
--text-secondary: #4a5568;
--text-muted: #718096;
--text-light: #a0aec0;
--bg-primary: #ffffff;
--bg-secondary: #f7fafc;
--bg-tertiary: #edf2f7;
--bg-overlay: rgba(255, 255, 255, 0.95);
--border-color: #e2e8f0;
--border-light: #f1f5f9;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
/* 间距系统 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 20px;
--spacing-2xl: 24px;
--spacing-3xl: 32px;
/* 圆角系统 */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
/* 动画 */
--transition-fast: 0.15s ease;
--transition-normal: 0.3s ease;
--transition-slow: 0.5s ease;
/* 字体渲染优化 */
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
/* 重置样式 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
padding: 0;
width: 400px;
min-height: 500px;
max-height: 600px;
overflow: hidden;
font-family: inherit;
background: var(--bg-secondary);
color: var(--text-primary);
}
#app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
/* 链接样式 */
a {
color: var(--primary-color);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--primary-dark);
}
/* 按钮基础样式重置 */
button {
font-family: inherit;
font-size: inherit;
line-height: inherit;
border: none;
background: none;
cursor: pointer;
transition: all var(--transition-normal);
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* 输入框基础样式 */
input,
textarea,
select {
font-family: inherit;
font-size: inherit;
line-height: inherit;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-primary);
color: var(--text-primary);
transition: all var(--transition-fast);
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: var(--radius-sm);
transition: background var(--transition-fast);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* 选择文本样式 */
::selection {
background: rgba(102, 126, 234, 0.2);
color: var(--text-primary);
}
/* 焦点可见性 */
:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* 动画关键帧 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 响应式断点 */
@media (max-width: 420px) {
:root {
--spacing-xs: 3px;
--spacing-sm: 6px;
--spacing-md: 10px;
--spacing-lg: 14px;
--spacing-xl: 18px;
--spacing-2xl: 22px;
--spacing-3xl: 28px;
}
}
/* 高对比度模式支持 */
@media (prefers-contrast: high) {
:root {
--border-color: #000000;
--text-muted: #000000;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -0,0 +1,50 @@
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import { defineConfig } from 'eslint/config';
import prettierConfig from 'eslint-config-prettier';
export default defineConfig([
// Global ignores - these apply to all configurations
{
ignores: [
'dist/**',
'.output/**',
'.wxt/**',
'node_modules/**',
'logs/**',
'*.log',
'.cache/**',
'.temp/**',
'.vscode/**',
'!.vscode/extensions.json',
'.idea/**',
'.DS_Store',
'Thumbs.db',
'*.zip',
'*.tar.gz',
'stats.html',
'stats-*.json',
'libs/**',
'workers/**',
'public/libs/**',
],
},
js.configs.recommended,
{
files: ['**/*.{js,mjs,cjs,ts,vue}'],
languageOptions: { globals: globals.browser },
},
...tseslint.configs.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
pluginVue.configs['flat/essential'],
{ files: ['**/*.vue'], languageOptions: { parserOptions: { parser: tseslint.parser } } },
// Prettier configuration - must be placed last to override previous rules
prettierConfig,
]);

View File

@@ -0,0 +1,233 @@
/* eslint-disable */
// click-helper.js
// This script is injected into the page to handle click operations
if (window.__CLICK_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__CLICK_HELPER_INITIALIZED__ = true;
/**
* Click on an element matching the selector or at specific coordinates
* @param {string} selector - CSS selector for the element to click
* @param {boolean} waitForNavigation - Whether to wait for navigation to complete after click
* @param {number} timeout - Timeout in milliseconds for waiting for the element or navigation
* @param {Object} coordinates - Optional coordinates for clicking at a specific position
* @param {number} coordinates.x - X coordinate relative to the viewport
* @param {number} coordinates.y - Y coordinate relative to the viewport
* @returns {Promise<Object>} - Result of the click operation
*/
async function clickElement(
selector,
waitForNavigation = false,
timeout = 5000,
coordinates = null,
) {
try {
let element = null;
let elementInfo = null;
let clickX, clickY;
if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
clickX = coordinates.x;
clickY = coordinates.y;
element = document.elementFromPoint(clickX, clickY);
if (element) {
const rect = element.getBoundingClientRect();
elementInfo = {
tagName: element.tagName,
id: element.id,
className: element.className,
text: element.textContent?.trim().substring(0, 100) || '',
href: element.href || null,
type: element.type || null,
isVisible: true,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
},
clickMethod: 'coordinates',
clickPosition: { x: clickX, y: clickY },
};
} else {
elementInfo = {
clickMethod: 'coordinates',
clickPosition: { x: clickX, y: clickY },
warning: 'No element found at the specified coordinates',
};
}
} else {
element = document.querySelector(selector);
if (!element) {
return {
error: `Element with selector "${selector}" not found`,
};
}
const rect = element.getBoundingClientRect();
elementInfo = {
tagName: element.tagName,
id: element.id,
className: element.className,
text: element.textContent?.trim().substring(0, 100) || '',
href: element.href || null,
type: element.type || null,
isVisible: true,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
},
clickMethod: 'selector',
};
// First sroll so that the element is in view, then check visibility.
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
await new Promise((resolve) => setTimeout(resolve, 100));
elementInfo.isVisible = isElementVisible(element);
if (!elementInfo.isVisible) {
return {
error: `Element with selector "${selector}" is not visible`,
elementInfo,
};
}
const updatedRect = element.getBoundingClientRect();
clickX = updatedRect.left + updatedRect.width / 2;
clickY = updatedRect.top + updatedRect.height / 2;
}
let navigationPromise;
if (waitForNavigation) {
navigationPromise = new Promise((resolve) => {
const beforeUnloadListener = () => {
window.removeEventListener('beforeunload', beforeUnloadListener);
resolve(true);
};
window.addEventListener('beforeunload', beforeUnloadListener);
setTimeout(() => {
window.removeEventListener('beforeunload', beforeUnloadListener);
resolve(false);
}, timeout);
});
}
if (element && elementInfo.clickMethod === 'selector') {
element.click();
} else {
simulateClick(clickX, clickY);
}
// Wait for navigation if needed
let navigationOccurred = false;
if (waitForNavigation) {
navigationOccurred = await navigationPromise;
}
return {
success: true,
message: 'Element clicked successfully',
elementInfo,
navigationOccurred,
};
} catch (error) {
return {
error: `Error clicking element: ${error.message}`,
};
}
}
/**
* Simulate a mouse click at specific coordinates
* @param {number} x - X coordinate relative to the viewport
* @param {number} y - Y coordinate relative to the viewport
*/
function simulateClick(x, y) {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
});
const element = document.elementFromPoint(x, y);
if (element) {
element.dispatchEvent(clickEvent);
} else {
document.dispatchEvent(clickEvent);
}
}
/**
* Check if an element is visible
* @param {Element} element - The element to check
* @returns {boolean} - Whether the element is visible
*/
function isElementVisible(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
}
if (
rect.bottom < 0 ||
rect.top > window.innerHeight ||
rect.right < 0 ||
rect.left > window.innerWidth
) {
return false;
}
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const elementAtPoint = document.elementFromPoint(centerX, centerY);
if (!elementAtPoint) return false;
return element === elementAtPoint || element.contains(elementAtPoint);
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'clickElement') {
clickElement(
request.selector,
request.waitForNavigation,
request.timeout,
request.coordinates,
)
.then(sendResponse)
.catch((error) => {
sendResponse({
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
} else if (request.action === 'chrome_click_element_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
}

View File

@@ -0,0 +1,205 @@
/* eslint-disable */
// fill-helper.js
// This script is injected into the page to handle form filling operations
if (window.__FILL_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__FILL_HELPER_INITIALIZED__ = true;
/**
* Fill an input element with the specified value
* @param {string} selector - CSS selector for the element to fill
* @param {string} value - Value to fill into the element
* @returns {Promise<Object>} - Result of the fill operation
*/
async function fillElement(selector, value) {
try {
// Find the element
const element = document.querySelector(selector);
if (!element) {
return {
error: `Element with selector "${selector}" not found`,
};
}
// Get element information
const rect = element.getBoundingClientRect();
const elementInfo = {
tagName: element.tagName,
id: element.id,
className: element.className,
type: element.type || null,
isVisible: isElementVisible(element),
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
},
};
// Check if element is visible
if (!elementInfo.isVisible) {
return {
error: `Element with selector "${selector}" is not visible`,
elementInfo,
};
}
// Check if element is an input, textarea, or select
const validTags = ['INPUT', 'TEXTAREA', 'SELECT'];
const validInputTypes = [
'text',
'email',
'password',
'number',
'search',
'tel',
'url',
'date',
'datetime-local',
'month',
'time',
'week',
'color',
];
if (!validTags.includes(element.tagName)) {
return {
error: `Element with selector "${selector}" is not a fillable element (must be INPUT, TEXTAREA, or SELECT)`,
elementInfo,
};
}
// For input elements, check if the type is valid
if (
element.tagName === 'INPUT' &&
!validInputTypes.includes(element.type) &&
element.type !== null
) {
return {
error: `Input element with selector "${selector}" has type "${element.type}" which is not fillable`,
elementInfo,
};
}
// Scroll element into view
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
await new Promise((resolve) => setTimeout(resolve, 100));
// Focus the element
element.focus();
// Fill the element based on its type
if (element.tagName === 'SELECT') {
// For select elements, find the option with matching value or text
let optionFound = false;
for (const option of element.options) {
if (option.value === value || option.text === value) {
element.value = option.value;
optionFound = true;
break;
}
}
if (!optionFound) {
return {
error: `No option with value or text "${value}" found in select element`,
elementInfo,
};
}
// Trigger change event
element.dispatchEvent(new Event('change', { bubbles: true }));
} else {
// For input and textarea elements
// Clear the current value
element.value = '';
element.dispatchEvent(new Event('input', { bubbles: true }));
// Set the new value
element.value = value;
// Trigger input and change events
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
// Blur the element
element.blur();
return {
success: true,
message: 'Element filled successfully',
elementInfo: {
...elementInfo,
value: element.value, // Include the final value in the response
},
};
} catch (error) {
return {
error: `Error filling element: ${error.message}`,
};
}
}
/**
* Check if an element is visible
* @param {Element} element - The element to check
* @returns {boolean} - Whether the element is visible
*/
function isElementVisible(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
}
// Check if element is within viewport
if (
rect.bottom < 0 ||
rect.top > window.innerHeight ||
rect.right < 0 ||
rect.left > window.innerWidth
) {
return false;
}
// Check if element is actually visible at its center point
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const elementAtPoint = document.elementFromPoint(centerX, centerY);
if (!elementAtPoint) return false;
return element === elementAtPoint || element.contains(elementAtPoint);
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'fillElement') {
fillElement(request.selector, request.value)
.then(sendResponse)
.catch((error) => {
sendResponse({
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
} else if (request.action === 'chrome_fill_or_select_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
}

View File

@@ -0,0 +1,65 @@
/* eslint-disable */
(() => {
// Prevent duplicate injection of the bridge itself.
if (window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__) return;
window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__ = true;
const EVENT_NAME = {
RESPONSE: 'chrome-mcp:response',
CLEANUP: 'chrome-mcp:cleanup',
EXECUTE: 'chrome-mcp:execute',
};
const pendingRequests = new Map();
const messageHandler = (request, _sender, sendResponse) => {
// --- Lifecycle Command ---
if (request.type === EVENT_NAME.CLEANUP) {
window.dispatchEvent(new CustomEvent(EVENT_NAME.CLEANUP));
// Acknowledge cleanup signal received, but don't hold the connection.
sendResponse({ success: true });
return true;
}
// --- Execution Command for MAIN world ---
if (request.targetWorld === 'MAIN') {
const requestId = `req-${Date.now()}-${Math.random()}`;
pendingRequests.set(requestId, sendResponse);
window.dispatchEvent(
new CustomEvent(EVENT_NAME.EXECUTE, {
detail: {
action: request.action,
payload: request.payload,
requestId: requestId,
},
}),
);
return true; // Async response is expected.
}
// Note: Requests for ISOLATED world are handled by the user's isolatedWorldCode script directly.
// This listener won't process them unless it's the only script in ISOLATED world.
};
chrome.runtime.onMessage.addListener(messageHandler);
// Listen for responses coming back from the MAIN world.
const responseHandler = (event) => {
const { requestId, data, error } = event.detail;
if (pendingRequests.has(requestId)) {
const sendResponse = pendingRequests.get(requestId);
sendResponse({ data, error });
pendingRequests.delete(requestId);
}
};
window.addEventListener(EVENT_NAME.RESPONSE, responseHandler);
// --- Self Cleanup ---
// When the cleanup signal arrives, this bridge must also clean itself up.
const cleanupHandler = () => {
chrome.runtime.onMessage.removeListener(messageHandler);
window.removeEventListener(EVENT_NAME.RESPONSE, responseHandler);
window.removeEventListener(EVENT_NAME.CLEANUP, cleanupHandler);
delete window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__;
};
window.addEventListener(EVENT_NAME.CLEANUP, cleanupHandler);
})();

View File

@@ -0,0 +1,354 @@
/* eslint-disable */
// interactive-elements-helper.js
// This script is injected into the page to find interactive elements.
// Final version by Calvin, featuring a multi-layered fallback strategy
// and comprehensive element support, built on a performant and reliable core.
(function () {
// Prevent re-initialization
if (window.__INTERACTIVE_ELEMENTS_HELPER_INITIALIZED__) {
return;
}
window.__INTERACTIVE_ELEMENTS_HELPER_INITIALIZED__ = true;
/**
* @typedef {Object} ElementInfo
* @property {string} type - The type of the element (e.g., 'button', 'link').
* @property {string} selector - A CSS selector to uniquely identify the element.
* @property {string} text - The visible text or accessible name of the element.
* @property {boolean} isInteractive - Whether the element is currently interactive.
* @property {Object} [coordinates] - The coordinates of the element if requested.
* @property {boolean} [disabled] - For elements that can be disabled.
* @property {string} [href] - For links.
* @property {boolean} [checked] - for checkboxes and radio buttons.
*/
/**
* Configuration for element types and their corresponding selectors.
* Now more comprehensive with common ARIA roles.
*/
const ELEMENT_CONFIG = {
button: 'button, input[type="button"], input[type="submit"], [role="button"]',
link: 'a[href], [role="link"]',
input:
'input:not([type="button"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"])',
checkbox: 'input[type="checkbox"], [role="checkbox"]',
radio: 'input[type="radio"], [role="radio"]',
textarea: 'textarea',
select: 'select',
tab: '[role="tab"]',
// Generic interactive elements: combines tabindex, common roles, and explicit handlers.
// This is the key to finding custom-built interactive components.
interactive: `[onclick], [tabindex]:not([tabindex^="-"]), [role="menuitem"], [role="slider"], [role="option"], [role="treeitem"]`,
};
// A combined selector for ANY interactive element, used in the fallback logic.
const ANY_INTERACTIVE_SELECTOR = Object.values(ELEMENT_CONFIG).join(', ');
// --- Core Helper Functions ---
/**
* Checks if an element is genuinely visible on the page.
* "Visible" means it's not styled with display:none, visibility:hidden, etc.
* This check intentionally IGNORES whether the element is within the current viewport.
* @param {Element} el The element to check.
* @returns {boolean} True if the element is visible.
*/
function isElementVisible(el) {
if (!el || !el.isConnected) return false;
const style = window.getComputedStyle(el);
if (
style.display === 'none' ||
style.visibility === 'hidden' ||
parseFloat(style.opacity) === 0
) {
return false;
}
const rect = el.getBoundingClientRect();
return rect.width > 0 || rect.height > 0 || el.tagName === 'A'; // Allow zero-size anchors as they can still be navigated
}
/**
* Checks if an element is considered interactive (not disabled or hidden from accessibility).
* @param {Element} el The element to check.
* @returns {boolean} True if the element is interactive.
*/
function isElementInteractive(el) {
if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') {
return false;
}
if (el.closest('[aria-hidden="true"]')) {
return false;
}
return true;
}
/**
* Generates a reasonably stable CSS selector for a given element.
* @param {Element} el The element.
* @returns {string} A CSS selector.
*/
function generateSelector(el) {
if (!(el instanceof Element)) return '';
if (el.id) {
const idSelector = `#${CSS.escape(el.id)}`;
if (document.querySelectorAll(idSelector).length === 1) return idSelector;
}
for (const attr of ['data-testid', 'data-cy', 'name']) {
const attrValue = el.getAttribute(attr);
if (attrValue) {
const attrSelector = `[${attr}="${CSS.escape(attrValue)}"]`;
if (document.querySelectorAll(attrSelector).length === 1) return attrSelector;
}
}
let path = '';
let current = el;
while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName !== 'BODY') {
let selector = current.tagName.toLowerCase();
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter(
(child) => child.tagName === current.tagName,
);
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1;
selector += `:nth-of-type(${index})`;
}
}
path = path ? `${selector} > ${path}` : selector;
current = parent;
}
return path ? `body > ${path}` : 'body';
}
/**
* Finds the accessible name for an element (label, aria-label, etc.).
* @param {Element} el The element.
* @returns {string} The accessible name.
*/
function getAccessibleName(el) {
const labelledby = el.getAttribute('aria-labelledby');
if (labelledby) {
const labelElement = document.getElementById(labelledby);
if (labelElement) return labelElement.textContent?.trim() || '';
}
const ariaLabel = el.getAttribute('aria-label');
if (ariaLabel) return ariaLabel.trim();
if (el.id) {
const label = document.querySelector(`label[for="${el.id}"]`);
if (label) return label.textContent?.trim() || '';
}
const parentLabel = el.closest('label');
if (parentLabel) return parentLabel.textContent?.trim() || '';
return (
el.getAttribute('placeholder') ||
el.getAttribute('value') ||
el.textContent?.trim() ||
el.getAttribute('title') ||
''
);
}
/**
* Simple subsequence matching for fuzzy search.
* @param {string} text The text to search within.
* @param {string} query The query subsequence.
* @returns {boolean}
*/
function fuzzyMatch(text, query) {
if (!text || !query) return false;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
let textIndex = 0;
let queryIndex = 0;
while (textIndex < lowerText.length && queryIndex < lowerQuery.length) {
if (lowerText[textIndex] === lowerQuery[queryIndex]) {
queryIndex++;
}
textIndex++;
}
return queryIndex === lowerQuery.length;
}
/**
* Creates the standardized info object for an element.
* Modified to handle the new 'text' type from the final fallback.
*/
function createElementInfo(el, type, includeCoordinates, isInteractiveOverride = null) {
const isActuallyInteractive = isElementInteractive(el);
const info = {
type,
selector: generateSelector(el),
text: getAccessibleName(el) || el.textContent?.trim(),
isInteractive: isInteractiveOverride !== null ? isInteractiveOverride : isActuallyInteractive,
disabled: el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true',
};
if (includeCoordinates) {
const rect = el.getBoundingClientRect();
info.coordinates = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
},
};
}
return info;
}
/**
* [CORE UTILITY] Finds interactive elements based on a set of types.
* This is our high-performance Layer 1 search function.
*/
function findInteractiveElements(options = {}) {
const { textQuery, includeCoordinates = true, types = Object.keys(ELEMENT_CONFIG) } = options;
const selectorsToFind = types
.map((type) => ELEMENT_CONFIG[type])
.filter(Boolean)
.join(', ');
if (!selectorsToFind) return [];
const targetElements = Array.from(document.querySelectorAll(selectorsToFind));
const uniqueElements = new Set(targetElements);
const results = [];
for (const el of uniqueElements) {
if (!isElementVisible(el) || !isElementInteractive(el)) continue;
const accessibleName = getAccessibleName(el);
if (textQuery && !fuzzyMatch(accessibleName, textQuery)) continue;
let elementType = 'unknown';
for (const [type, typeSelector] of Object.entries(ELEMENT_CONFIG)) {
if (el.matches(typeSelector)) {
elementType = type;
break;
}
}
results.push(createElementInfo(el, elementType, includeCoordinates));
}
return results;
}
/**
* [ORCHESTRATOR] The main entry point that implements the 3-layer fallback logic.
* @param {object} options - The main search options.
* @returns {ElementInfo[]}
*/
function findElementsByTextWithFallback(options = {}) {
const { textQuery, includeCoordinates = true } = options;
if (!textQuery) {
return findInteractiveElements({ ...options, types: Object.keys(ELEMENT_CONFIG) });
}
// --- Layer 1: High-reliability search for interactive elements matching text ---
let results = findInteractiveElements({ ...options, types: Object.keys(ELEMENT_CONFIG) });
if (results.length > 0) {
return results;
}
// --- Layer 2: Find text, then find its interactive ancestor ---
const lowerCaseText = textQuery.toLowerCase();
const xPath = `//text()[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '${lowerCaseText}')]`;
const textNodes = document.evaluate(
xPath,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null,
);
const interactiveElements = new Set();
if (textNodes.snapshotLength > 0) {
for (let i = 0; i < textNodes.snapshotLength; i++) {
const parentElement = textNodes.snapshotItem(i).parentElement;
if (parentElement) {
const interactiveAncestor = parentElement.closest(ANY_INTERACTIVE_SELECTOR);
if (
interactiveAncestor &&
isElementVisible(interactiveAncestor) &&
isElementInteractive(interactiveAncestor)
) {
interactiveElements.add(interactiveAncestor);
}
}
}
if (interactiveElements.size > 0) {
return Array.from(interactiveElements).map((el) => {
let elementType = 'interactive';
for (const [type, typeSelector] of Object.entries(ELEMENT_CONFIG)) {
if (el.matches(typeSelector)) {
elementType = type;
break;
}
}
return createElementInfo(el, elementType, includeCoordinates);
});
}
}
// --- Layer 3: Final fallback, return any element containing the text ---
const leafElements = new Set();
for (let i = 0; i < textNodes.snapshotLength; i++) {
const parentElement = textNodes.snapshotItem(i).parentElement;
if (parentElement && isElementVisible(parentElement)) {
leafElements.add(parentElement);
}
}
const finalElements = Array.from(leafElements).filter((el) => {
return ![...leafElements].some((otherEl) => el !== otherEl && el.contains(otherEl));
});
return finalElements.map((el) => createElementInfo(el, 'text', includeCoordinates, true));
}
// --- Chrome Message Listener ---
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'getInteractiveElements') {
try {
let elements;
if (request.selector) {
// If a selector is provided, bypass the text-based logic and use a direct query.
const foundEls = Array.from(document.querySelectorAll(request.selector));
elements = foundEls.map((el) =>
createElementInfo(
el,
'selected',
request.includeCoordinates !== false,
isElementInteractive(el),
),
);
} else {
// Otherwise, use our powerful multi-layered text search
elements = findElementsByTextWithFallback(request);
}
sendResponse({ success: true, elements });
} catch (error) {
console.error('Error in getInteractiveElements:', error);
sendResponse({ success: false, error: error.message });
}
return true; // Async response
} else if (request.action === 'chrome_get_interactive_elements_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
console.log('Interactive elements helper script loaded');
})();

View File

@@ -0,0 +1,291 @@
/* eslint-disable */
// keyboard-helper.js
// This script is injected into the page to handle keyboard event simulation
if (window.__KEYBOARD_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__KEYBOARD_HELPER_INITIALIZED__ = true;
// A map for special keys to their KeyboardEvent properties
// Key names should be lowercase for matching
const SPECIAL_KEY_MAP = {
enter: { key: 'Enter', code: 'Enter', keyCode: 13 },
tab: { key: 'Tab', code: 'Tab', keyCode: 9 },
esc: { key: 'Escape', code: 'Escape', keyCode: 27 },
escape: { key: 'Escape', code: 'Escape', keyCode: 27 },
space: { key: ' ', code: 'Space', keyCode: 32 },
backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 },
delete: { key: 'Delete', code: 'Delete', keyCode: 46 },
del: { key: 'Delete', code: 'Delete', keyCode: 46 },
up: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
arrowup: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
down: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
arrowdown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
left: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
right: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
arrowright: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
home: { key: 'Home', code: 'Home', keyCode: 36 },
end: { key: 'End', code: 'End', keyCode: 35 },
pageup: { key: 'PageUp', code: 'PageUp', keyCode: 33 },
pagedown: { key: 'PageDown', code: 'PageDown', keyCode: 34 },
insert: { key: 'Insert', code: 'Insert', keyCode: 45 },
// Function keys
...Object.fromEntries(
Array.from({ length: 12 }, (_, i) => [
`f${i + 1}`,
{ key: `F${i + 1}`, code: `F${i + 1}`, keyCode: 112 + i },
]),
),
};
const MODIFIER_KEYS = {
ctrl: 'ctrlKey',
control: 'ctrlKey',
alt: 'altKey',
shift: 'shiftKey',
meta: 'metaKey',
command: 'metaKey',
cmd: 'metaKey',
};
/**
* Parses a key string (e.g., "Ctrl+Shift+A", "Enter") into a main key and modifiers.
* @param {string} keyString - String representation of a single key press (can include modifiers).
* @returns { {key: string, code: string, keyCode: number, charCode?: number, modifiers: {ctrlKey:boolean, altKey:boolean, shiftKey:boolean, metaKey:boolean}} | null }
* Returns null if the keyString is invalid or represents only modifiers.
*/
function parseSingleKeyCombination(keyString) {
const parts = keyString.split('+').map((part) => part.trim().toLowerCase());
const modifiers = {
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
};
let mainKeyPart = null;
for (const part of parts) {
if (MODIFIER_KEYS[part]) {
modifiers[MODIFIER_KEYS[part]] = true;
} else if (mainKeyPart === null) {
// First non-modifier is the main key
mainKeyPart = part;
} else {
// Invalid format: multiple main keys in a single combination (e.g., "Ctrl+A+B")
console.error(`Invalid key combination string: ${keyString}. Multiple main keys found.`);
return null;
}
}
if (!mainKeyPart) {
// This case could happen if the keyString is something like "Ctrl+" or just "Ctrl"
// If the intent was to press JUST 'Control', the input should be 'Control' not 'Control+'
// Let's check if mainKeyPart is actually a modifier name used as a main key
if (Object.keys(MODIFIER_KEYS).includes(parts[parts.length - 1]) && parts.length === 1) {
mainKeyPart = parts[parts.length - 1]; // e.g. user wants to press "Control" key itself
// For "Control" key itself, key: "Control", code: "ControlLeft" (or Right)
if (mainKeyPart === 'ctrl' || mainKeyPart === 'control')
return { key: 'Control', code: 'ControlLeft', keyCode: 17, modifiers };
if (mainKeyPart === 'alt') return { key: 'Alt', code: 'AltLeft', keyCode: 18, modifiers };
if (mainKeyPart === 'shift')
return { key: 'Shift', code: 'ShiftLeft', keyCode: 16, modifiers };
if (mainKeyPart === 'meta' || mainKeyPart === 'command' || mainKeyPart === 'cmd')
return { key: 'Meta', code: 'MetaLeft', keyCode: 91, modifiers };
} else {
console.error(`Invalid key combination string: ${keyString}. No main key specified.`);
return null;
}
}
const specialKey = SPECIAL_KEY_MAP[mainKeyPart];
if (specialKey) {
return { ...specialKey, modifiers };
}
// For single characters or other unmapped keys
if (mainKeyPart.length === 1) {
const charCode = mainKeyPart.charCodeAt(0);
// If Shift is active and it's a letter, use the uppercase version for 'key'
// This mimics more closely how keyboards behave.
let keyChar = mainKeyPart;
if (modifiers.shiftKey && mainKeyPart.match(/^[a-z]$/i)) {
keyChar = mainKeyPart.toUpperCase();
}
return {
key: keyChar,
code: `Key${mainKeyPart.toUpperCase()}`, // 'a' -> KeyA, 'A' -> KeyA
keyCode: charCode,
charCode: charCode, // charCode is legacy, but some old systems might use it
modifiers,
};
}
console.error(`Unknown key: ${mainKeyPart} in string "${keyString}"`);
return null; // Or handle as an error
}
/**
* Simulates a single key press (keydown, (keypress), keyup) for a parsed key.
* @param { {key: string, code: string, keyCode: number, charCode?: number, modifiers: object} } parsedKeyInfo
* @param {Element} element - Target element.
* @returns {{success: boolean, error?: string}}
*/
function dispatchKeyEvents(parsedKeyInfo, element) {
if (!parsedKeyInfo) return { success: false, error: 'Invalid key info provided for dispatch.' };
const { key, code, keyCode, charCode, modifiers } = parsedKeyInfo;
const eventOptions = {
key: key,
code: code,
bubbles: true,
cancelable: true,
composed: true, // Important for shadow DOM
view: window,
...modifiers, // ctrlKey, altKey, shiftKey, metaKey
// keyCode/which are deprecated but often set for compatibility
keyCode: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0),
which: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0),
};
try {
const kdRes = element.dispatchEvent(new KeyboardEvent('keydown', eventOptions));
// keypress is deprecated, but simulate if it's a character key or Enter
// Only dispatch if keydown was not cancelled and it's a character producing key
if (kdRes && (key.length === 1 || key === 'Enter' || key === ' ')) {
const keypressOptions = { ...eventOptions };
if (charCode) keypressOptions.charCode = charCode;
element.dispatchEvent(new KeyboardEvent('keypress', keypressOptions));
}
element.dispatchEvent(new KeyboardEvent('keyup', eventOptions));
return { success: true };
} catch (error) {
console.error(`Error dispatching key events for "${key}":`, error);
return {
success: false,
error: `Error dispatching key events for "${key}": ${error.message}`,
};
}
}
/**
* Simulate keyboard events on an element or document
* @param {string} keysSequenceString - String representation of key(s) (e.g., "Enter", "Ctrl+C, A, B")
* @param {Element} targetElement - Element to dispatch events on (optional)
* @param {number} delay - Delay between key sequences in milliseconds (optional)
* @returns {Promise<Object>} - Result of the keyboard operation
*/
async function simulateKeyboard(keysSequenceString, targetElement = null, delay = 0) {
try {
const element = targetElement || document.activeElement || document.body;
if (element !== document.activeElement && typeof element.focus === 'function') {
element.focus();
await new Promise((resolve) => setTimeout(resolve, 50)); // Small delay for focus
}
const keyCombinations = keysSequenceString
.split(',')
.map((k) => k.trim())
.filter((k) => k.length > 0);
const operationResults = [];
for (let i = 0; i < keyCombinations.length; i++) {
const comboString = keyCombinations[i];
const parsedKeyInfo = parseSingleKeyCombination(comboString);
if (!parsedKeyInfo) {
operationResults.push({
keyCombination: comboString,
success: false,
error: `Invalid key string or combination: ${comboString}`,
});
continue; // Skip to next combination in sequence
}
const dispatchResult = dispatchKeyEvents(parsedKeyInfo, element);
operationResults.push({
keyCombination: comboString,
...dispatchResult,
});
if (dispatchResult.error) {
// Optionally, decide if sequence should stop on first error
// For now, we continue but log the error in results
console.warn(
`Failed to simulate key combination "${comboString}": ${dispatchResult.error}`,
);
}
if (delay > 0 && i < keyCombinations.length - 1) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Check if all individual operations were successful
const overallSuccess = operationResults.every((r) => r.success);
return {
success: overallSuccess,
message: overallSuccess
? `Keyboard events simulated successfully: ${keysSequenceString}`
: `Some keyboard events failed for: ${keysSequenceString}`,
results: operationResults, // Detailed results for each key combination
targetElement: {
tagName: element.tagName,
id: element.id,
className: element.className,
type: element.type, // if applicable e.g. for input
},
};
} catch (error) {
console.error('Error in simulateKeyboard:', error);
return {
success: false,
error: `Error simulating keyboard events: ${error.message}`,
results: [],
};
}
}
// Listener for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'simulateKeyboard') {
let targetEl = null;
if (request.selector) {
targetEl = document.querySelector(request.selector);
if (!targetEl) {
sendResponse({
success: false,
error: `Element with selector "${request.selector}" not found`,
results: [],
});
return true; // Keep channel open for async response
}
}
simulateKeyboard(request.keys, targetEl, request.delay)
.then(sendResponse)
.catch((error) => {
// This catch is for unexpected errors in simulateKeyboard promise chain itself
console.error('Unexpected error in simulateKeyboard promise chain:', error);
sendResponse({
success: false,
error: `Unexpected error during keyboard simulation: ${error.message}`,
results: [],
});
});
return true; // Indicates async response is expected
} else if (request.action === 'chrome_keyboard_ping') {
sendResponse({ status: 'pong', initialized: true }); // Respond that it's initialized
return false; // Synchronous response
}
// Not our message, or no async response needed
return false;
});
}

View File

@@ -0,0 +1,129 @@
/* eslint-disable */
/**
* Network Capture Helper
*
* This script helps replay network requests with the original cookies and headers.
*/
// Prevent duplicate initialization
if (window.__NETWORK_CAPTURE_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__NETWORK_CAPTURE_HELPER_INITIALIZED__ = true;
/**
* Replay a network request
* @param {string} url - The URL to send the request to
* @param {string} method - The HTTP method to use
* @param {Object} headers - The headers to include in the request
* @param {any} body - The body of the request
* @param {number} timeout - Timeout in milliseconds (default: 30000)
* @returns {Promise<Object>} - The response data
*/
async function replayNetworkRequest(url, method, headers, body, timeout = 30000) {
try {
// Create fetch options
const options = {
method: method,
headers: headers || {},
credentials: 'include', // Include cookies
mode: 'cors',
cache: 'no-cache',
};
// Add body for non-GET requests
if (method !== 'GET' && method !== 'HEAD' && body !== undefined) {
options.body = body;
}
// 创建一个带超时的 fetch
const fetchWithTimeout = async (url, options, timeout) => {
const controller = new AbortController();
const signal = controller.signal;
// 设置超时
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { ...options, signal });
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
};
// 发送带超时的请求
const response = await fetchWithTimeout(url, options, timeout);
// Process response
const responseData = {
status: response.status,
statusText: response.statusText,
headers: {},
};
// Get response headers
response.headers.forEach((value, key) => {
responseData.headers[key] = value;
});
// Try to get response body based on content type
const contentType = response.headers.get('content-type') || '';
try {
if (contentType.includes('application/json')) {
responseData.body = await response.json();
} else if (
contentType.includes('text/') ||
contentType.includes('application/xml') ||
contentType.includes('application/javascript')
) {
responseData.body = await response.text();
} else {
// For binary data, just indicate it was received but not parsed
responseData.body = '[Binary data not displayed]';
}
} catch (error) {
responseData.body = `[Error parsing response body: ${error.message}]`;
}
return {
success: true,
response: responseData,
};
} catch (error) {
console.error('Error replaying request:', error);
return {
success: false,
error: `Error replaying request: ${error.message}`,
};
}
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
// Respond to ping message
if (request.action === 'chrome_network_request_ping') {
sendResponse({ status: 'pong' });
return false; // Synchronous response
} else if (request.action === 'sendPureNetworkRequest') {
replayNetworkRequest(
request.url,
request.method,
request.headers,
request.body,
request.timeout,
)
.then(sendResponse)
.catch((error) => {
sendResponse({
success: false,
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
}
});
}

View File

@@ -0,0 +1,160 @@
/* eslint-disable */
/**
* Screenshot helper content script
* Handles page preparation, scrolling, element positioning, etc.
*/
if (window.__SCREENSHOT_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__SCREENSHOT_HELPER_INITIALIZED__ = true;
// Save original styles
let originalOverflowStyle = '';
let hiddenFixedElements = [];
/**
* Get fixed/sticky positioned elements
* @returns Array of fixed/sticky elements
*/
function getFixedElements() {
const fixed = [];
document.querySelectorAll('*').forEach((el) => {
const htmlEl = el;
const style = window.getComputedStyle(htmlEl);
if (style.position === 'fixed' || style.position === 'sticky') {
// Filter out tiny or invisible elements, and elements that are part of the extension UI
if (
htmlEl.offsetWidth > 1 &&
htmlEl.offsetHeight > 1 &&
!htmlEl.id.startsWith('chrome-mcp-')
) {
fixed.push({
element: htmlEl,
originalDisplay: htmlEl.style.display,
originalVisibility: htmlEl.style.visibility,
});
}
}
});
return fixed;
}
/**
* Hide fixed/sticky elements
*/
function hideFixedElements() {
hiddenFixedElements = getFixedElements();
hiddenFixedElements.forEach((item) => {
item.element.style.display = 'none';
});
}
/**
* Restore fixed/sticky elements
*/
function showFixedElements() {
hiddenFixedElements.forEach((item) => {
item.element.style.display = item.originalDisplay || '';
});
hiddenFixedElements = [];
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
// Respond to ping message
if (request.action === 'chrome_screenshot_ping') {
sendResponse({ status: 'pong' });
return false; // Synchronous response
}
// Prepare page for capture
else if (request.action === 'preparePageForCapture') {
originalOverflowStyle = document.documentElement.style.overflow;
document.documentElement.style.overflow = 'hidden'; // Hide main scrollbar
if (request.options?.fullPage) {
// Only hide fixed elements for full page to avoid flicker
hideFixedElements();
}
// Give styles a moment to apply
setTimeout(() => {
sendResponse({ success: true });
}, 50);
return true; // Async response
}
// Get page details
else if (request.action === 'getPageDetails') {
const body = document.body;
const html = document.documentElement;
sendResponse({
totalWidth: Math.max(
body.scrollWidth,
body.offsetWidth,
html.clientWidth,
html.scrollWidth,
html.offsetWidth,
),
totalHeight: Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight,
),
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio || 1,
currentScrollX: window.scrollX,
currentScrollY: window.scrollY,
});
}
// Get element details
else if (request.action === 'getElementDetails') {
const element = document.querySelector(request.selector);
if (element) {
element.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' });
setTimeout(() => {
// Wait for scroll
const rect = element.getBoundingClientRect();
sendResponse({
rect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
devicePixelRatio: window.devicePixelRatio || 1,
});
}, 200); // Increased delay for scrollIntoView
return true; // Async response
} else {
sendResponse({ error: `Element with selector "${request.selector}" not found.` });
}
return true; // Async response
}
// Scroll page
else if (request.action === 'scrollPage') {
window.scrollTo({ left: request.x, top: request.y, behavior: 'instant' });
// Wait for scroll and potential reflows/lazy-loading
setTimeout(() => {
sendResponse({
success: true,
newScrollX: window.scrollX,
newScrollY: window.scrollY,
});
}, request.scrollDelay || 300); // Configurable delay
return true; // Async response
}
// Reset page
else if (request.action === 'resetPageAfterCapture') {
document.documentElement.style.overflow = originalOverflowStyle;
showFixedElements();
if (typeof request.scrollX !== 'undefined' && typeof request.scrollY !== 'undefined') {
window.scrollTo({ left: request.scrollX, top: request.scrollY, behavior: 'instant' });
}
sendResponse({ success: true });
}
return false; // Synchronous response
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "chrome-mcp-server",
"description": "a chrome extension to use your own chrome as a mcp server",
"author": "hangye",
"private": true,
"version": "0.0.6",
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "vue-tsc --noEmit",
"postinstall": "wxt prepare",
"lint": "npx eslint .",
"lint:fix": "npx eslint . --fix",
"format": "npx prettier --write .",
"format:check": "npx prettier --check ."
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"@xenova/transformers": "^2.17.2",
"chrome-mcp-shared": "workspace:*",
"date-fns": "^4.1.0",
"hnswlib-wasm-static": "0.8.5",
"vue": "^3.5.13",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/chrome": "^0.0.318",
"@wxt-dev/module-vue": "^1.0.2",
"dotenv": "^16.5.0",
"vite-plugin-static-copy": "^3.0.0",
"vue-tsc": "^2.2.8",
"wxt": "^0.20.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 319 KiB

View File

@@ -0,0 +1,3 @@
{
"extends": "./.wxt/tsconfig.json"
}

View File

@@ -0,0 +1,586 @@
/**
* 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;
}

View File

@@ -0,0 +1,273 @@
/**
* Chrome Extension i18n utility
* Provides safe access to chrome.i18n.getMessage with fallbacks
*/
// Fallback messages for when Chrome APIs aren't available (English)
const fallbackMessages: Record<string, string> = {
// Extension metadata
extensionName: 'chrome-mcp-server',
extensionDescription: 'Exposes browser capabilities with your own chrome',
// Section headers
nativeServerConfigLabel: 'Native Server Configuration',
semanticEngineLabel: 'Semantic Engine',
embeddingModelLabel: 'Embedding Model',
indexDataManagementLabel: 'Index Data Management',
modelCacheManagementLabel: 'Model Cache Management',
// Status labels
statusLabel: 'Status',
runningStatusLabel: 'Running Status',
connectionStatusLabel: 'Connection Status',
lastUpdatedLabel: 'Last Updated:',
// Connection states
connectButton: 'Connect',
disconnectButton: 'Disconnect',
connectingStatus: 'Connecting...',
connectedStatus: 'Connected',
disconnectedStatus: 'Disconnected',
detectingStatus: 'Detecting...',
// Server states
serviceRunningStatus: 'Service Running (Port: {0})',
serviceNotConnectedStatus: 'Service Not Connected',
connectedServiceNotStartedStatus: 'Connected, Service Not Started',
// Configuration labels
mcpServerConfigLabel: 'MCP Server Configuration',
connectionPortLabel: 'Connection Port',
refreshStatusButton: 'Refresh Status',
copyConfigButton: 'Copy Configuration',
// Action buttons
retryButton: 'Retry',
cancelButton: 'Cancel',
confirmButton: 'Confirm',
saveButton: 'Save',
closeButton: 'Close',
resetButton: 'Reset',
// Progress states
initializingStatus: 'Initializing...',
processingStatus: 'Processing...',
loadingStatus: 'Loading...',
clearingStatus: 'Clearing...',
cleaningStatus: 'Cleaning...',
downloadingStatus: 'Downloading...',
// Semantic engine states
semanticEngineReadyStatus: 'Semantic Engine Ready',
semanticEngineInitializingStatus: 'Semantic Engine Initializing...',
semanticEngineInitFailedStatus: 'Semantic Engine Initialization Failed',
semanticEngineNotInitStatus: 'Semantic Engine Not Initialized',
initSemanticEngineButton: 'Initialize Semantic Engine',
reinitializeButton: 'Reinitialize',
// Model states
downloadingModelStatus: 'Downloading Model... {0}%',
switchingModelStatus: 'Switching Model...',
modelLoadedStatus: 'Model Loaded',
modelFailedStatus: 'Model Failed to Load',
// Model descriptions
lightweightModelDescription: 'Lightweight Multilingual Model',
betterThanSmallDescription: 'Slightly larger than e5-small, but better performance',
multilingualModelDescription: 'Multilingual Semantic Model',
// Performance levels
fastPerformance: 'Fast',
balancedPerformance: 'Balanced',
accuratePerformance: 'Accurate',
// Error messages
networkErrorMessage: 'Network connection error, please check network and retry',
modelCorruptedErrorMessage: 'Model file corrupted or incomplete, please retry download',
unknownErrorMessage: 'Unknown error, please check if your network can access HuggingFace',
permissionDeniedErrorMessage: 'Permission denied',
timeoutErrorMessage: 'Operation timed out',
// Data statistics
indexedPagesLabel: 'Indexed Pages',
indexSizeLabel: 'Index Size',
activeTabsLabel: 'Active Tabs',
vectorDocumentsLabel: 'Vector Documents',
cacheSizeLabel: 'Cache Size',
cacheEntriesLabel: 'Cache Entries',
// Data management
clearAllDataButton: 'Clear All Data',
clearAllCacheButton: 'Clear All Cache',
cleanExpiredCacheButton: 'Clean Expired Cache',
exportDataButton: 'Export Data',
importDataButton: 'Import Data',
// Dialog titles
confirmClearDataTitle: 'Confirm Clear Data',
settingsTitle: 'Settings',
aboutTitle: 'About',
helpTitle: 'Help',
// Dialog messages
clearDataWarningMessage:
'This operation will clear all indexed webpage content and vector data, including:',
clearDataList1: 'All webpage text content index',
clearDataList2: 'Vector embedding data',
clearDataList3: 'Search history and cache',
clearDataIrreversibleWarning:
'This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.',
confirmClearButton: 'Confirm Clear',
// Cache states
cacheDetailsLabel: 'Cache Details',
noCacheDataMessage: 'No cache data',
loadingCacheInfoStatus: 'Loading cache information...',
processingCacheStatus: 'Processing cache...',
expiredLabel: 'Expired',
// Browser integration
bookmarksBarLabel: 'Bookmarks Bar',
newTabLabel: 'New Tab',
currentPageLabel: 'Current Page',
// Accessibility
menuLabel: 'Menu',
navigationLabel: 'Navigation',
mainContentLabel: 'Main Content',
// Future features
languageSelectorLabel: 'Language',
themeLabel: 'Theme',
lightTheme: 'Light',
darkTheme: 'Dark',
autoTheme: 'Auto',
advancedSettingsLabel: 'Advanced Settings',
debugModeLabel: 'Debug Mode',
verboseLoggingLabel: 'Verbose Logging',
// Notifications
successNotification: 'Operation completed successfully',
warningNotification: 'Warning: Please review before proceeding',
infoNotification: 'Information',
configCopiedNotification: 'Configuration copied to clipboard',
dataClearedNotification: 'Data cleared successfully',
// Units
bytesUnit: 'bytes',
kilobytesUnit: 'KB',
megabytesUnit: 'MB',
gigabytesUnit: 'GB',
itemsUnit: 'items',
pagesUnit: 'pages',
// Legacy keys for backwards compatibility
nativeServerConfig: 'Native Server Configuration',
runningStatus: 'Running Status',
refreshStatus: 'Refresh Status',
lastUpdated: 'Last Updated:',
mcpServerConfig: 'MCP Server Configuration',
connectionPort: 'Connection Port',
connecting: 'Connecting...',
disconnect: 'Disconnect',
connect: 'Connect',
semanticEngine: 'Semantic Engine',
embeddingModel: 'Embedding Model',
retry: 'Retry',
indexDataManagement: 'Index Data Management',
clearing: 'Clearing...',
clearAllData: 'Clear All Data',
copyConfig: 'Copy Configuration',
serviceRunning: 'Service Running (Port: {0})',
connectedServiceNotStarted: 'Connected, Service Not Started',
serviceNotConnected: 'Service Not Connected',
detecting: 'Detecting...',
lightweightModel: 'Lightweight Multilingual Model',
betterThanSmall: 'Slightly larger than e5-small, but better performance',
multilingualModel: 'Multilingual Semantic Model',
fast: 'Fast',
balanced: 'Balanced',
accurate: 'Accurate',
semanticEngineReady: 'Semantic Engine Ready',
semanticEngineInitializing: 'Semantic Engine Initializing...',
semanticEngineInitFailed: 'Semantic Engine Initialization Failed',
semanticEngineNotInit: 'Semantic Engine Not Initialized',
downloadingModel: 'Downloading Model... {0}%',
switchingModel: 'Switching Model...',
networkError: 'Network connection error, please check network and retry',
modelCorrupted: 'Model file corrupted or incomplete, please retry download',
unknownError: 'Unknown error, please check if your network can access HuggingFace',
reinitialize: 'Reinitialize',
initializing: 'Initializing...',
initSemanticEngine: 'Initialize Semantic Engine',
indexedPages: 'Indexed Pages',
indexSize: 'Index Size',
activeTabs: 'Active Tabs',
vectorDocuments: 'Vector Documents',
confirmClearData: 'Confirm Clear Data',
clearDataWarning:
'This operation will clear all indexed webpage content and vector data, including:',
clearDataIrreversible:
'This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.',
confirmClear: 'Confirm Clear',
cancel: 'Cancel',
confirm: 'Confirm',
processing: 'Processing...',
modelCacheManagement: 'Model Cache Management',
cacheSize: 'Cache Size',
cacheEntries: 'Cache Entries',
cacheDetails: 'Cache Details',
noCacheData: 'No cache data',
loadingCacheInfo: 'Loading cache information...',
processingCache: 'Processing cache...',
cleaning: 'Cleaning...',
cleanExpiredCache: 'Clean Expired Cache',
clearAllCache: 'Clear All Cache',
expired: 'Expired',
bookmarksBar: 'Bookmarks Bar',
};
/**
* Safe i18n message getter with fallback support
* @param key Message key
* @param substitutions Optional substitution values
* @returns Localized message or fallback
*/
export function getMessage(key: string, substitutions?: string[]): string {
try {
// Check if Chrome extension APIs are available
if (typeof chrome !== 'undefined' && chrome.i18n && chrome.i18n.getMessage) {
const message = chrome.i18n.getMessage(key, substitutions);
if (message) {
return message;
}
}
} catch (error) {
console.warn(`Failed to get i18n message for key "${key}":`, error);
}
// Fallback to English messages
let fallback = fallbackMessages[key] || key;
// Handle substitutions in fallback messages
if (substitutions && substitutions.length > 0) {
substitutions.forEach((value, index) => {
fallback = fallback.replace(`{${index}}`, value);
});
}
return fallback;
}
/**
* Check if Chrome extension i18n APIs are available
*/
export function isI18nAvailable(): boolean {
try {
return (
typeof chrome !== 'undefined' && chrome.i18n && typeof chrome.i18n.getMessage === 'function'
);
} catch {
return false;
}
}

View File

@@ -0,0 +1,194 @@
/**
* Image processing utility functions
*/
/**
* Create ImageBitmap from data URL (for OffscreenCanvas)
* @param dataUrl Image data URL
* @returns Created ImageBitmap object
*/
export async function createImageBitmapFromUrl(dataUrl: string): Promise<ImageBitmap> {
const response = await fetch(dataUrl);
const blob = await response.blob();
return await createImageBitmap(blob);
}
/**
* Stitch multiple image parts (dataURL) onto a single canvas
* @param parts Array of image parts, each containing dataUrl and y coordinate
* @param totalWidthPx Total width (pixels)
* @param totalHeightPx Total height (pixels)
* @returns Stitched canvas
*/
export async function stitchImages(
parts: { dataUrl: string; y: number }[],
totalWidthPx: number,
totalHeightPx: number,
): Promise<OffscreenCanvas> {
const canvas = new OffscreenCanvas(totalWidthPx, totalHeightPx);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Unable to get canvas context');
}
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const part of parts) {
try {
const img = await createImageBitmapFromUrl(part.dataUrl);
const sx = 0;
const sy = 0;
const sWidth = img.width;
let sHeight = img.height;
const dy = part.y;
if (dy + sHeight > totalHeightPx) {
sHeight = totalHeightPx - dy;
}
if (sHeight <= 0) continue;
ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, dy, sWidth, sHeight);
} catch (error) {
console.error('Error stitching image part:', error, part);
}
}
return canvas;
}
/**
* Crop image (from dataURL) to specified rectangle and resize
* @param originalDataUrl Original image data URL
* @param cropRectPx Crop rectangle (physical pixels)
* @param dpr Device pixel ratio
* @param targetWidthOpt Optional target output width (CSS pixels)
* @param targetHeightOpt Optional target output height (CSS pixels)
* @returns Cropped canvas
*/
export async function cropAndResizeImage(
originalDataUrl: string,
cropRectPx: { x: number; y: number; width: number; height: number },
dpr: number = 1,
targetWidthOpt?: number,
targetHeightOpt?: number,
): Promise<OffscreenCanvas> {
const img = await createImageBitmapFromUrl(originalDataUrl);
let sx = cropRectPx.x;
let sy = cropRectPx.y;
let sWidth = cropRectPx.width;
let sHeight = cropRectPx.height;
// Ensure crop area is within image boundaries
if (sx < 0) {
sWidth += sx;
sx = 0;
}
if (sy < 0) {
sHeight += sy;
sy = 0;
}
if (sx + sWidth > img.width) {
sWidth = img.width - sx;
}
if (sy + sHeight > img.height) {
sHeight = img.height - sy;
}
if (sWidth <= 0 || sHeight <= 0) {
throw new Error(
'Invalid calculated crop size (<=0). Element may not be visible or fully captured.',
);
}
const finalCanvasWidthPx = targetWidthOpt ? targetWidthOpt * dpr : sWidth;
const finalCanvasHeightPx = targetHeightOpt ? targetHeightOpt * dpr : sHeight;
const canvas = new OffscreenCanvas(finalCanvasWidthPx, finalCanvasHeightPx);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Unable to get canvas context');
}
ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, finalCanvasWidthPx, finalCanvasHeightPx);
return canvas;
}
/**
* Convert canvas to data URL
* @param canvas Canvas
* @param format Image format
* @param quality JPEG quality (0-1)
* @returns Data URL
*/
export async function canvasToDataURL(
canvas: OffscreenCanvas,
format: string = 'image/png',
quality?: number,
): Promise<string> {
const blob = await canvas.convertToBlob({
type: format,
quality: format === 'image/jpeg' ? quality : undefined,
});
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
/**
* Compresses an image by scaling it and converting it to a target format with a specific quality.
* This is the most effective way to reduce image data size for transport or storage.
*
* @param {string} imageDataUrl - The original image data URL (e.g., from captureVisibleTab).
* @param {object} options - Compression options.
* @param {number} [options.scale=1.0] - The scaling factor for dimensions (e.g., 0.7 for 70%).
* @param {number} [options.quality=0.8] - The quality for lossy formats like JPEG (0.0 to 1.0).
* @param {string} [options.format='image/jpeg'] - The target image format.
* @returns {Promise<{dataUrl: string, mimeType: string}>} A promise that resolves to the compressed image data URL and its MIME type.
*/
export async function compressImage(
imageDataUrl: string,
options: { scale?: number; quality?: number; format?: 'image/jpeg' | 'image/webp' },
): Promise<{ dataUrl: string; mimeType: string }> {
const { scale = 1.0, quality = 0.8, format = 'image/jpeg' } = options;
// 1. Create an ImageBitmap from the original data URL for efficient drawing.
const imageBitmap = await createImageBitmapFromUrl(imageDataUrl);
// 2. Calculate the new dimensions based on the scale factor.
const newWidth = Math.round(imageBitmap.width * scale);
const newHeight = Math.round(imageBitmap.height * scale);
// 3. Use OffscreenCanvas for performance, as it doesn't need to be in the DOM.
const canvas = new OffscreenCanvas(newWidth, newHeight);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get 2D context from OffscreenCanvas');
}
// 4. Draw the original image onto the smaller canvas, effectively resizing it.
ctx.drawImage(imageBitmap, 0, 0, newWidth, newHeight);
// 5. Export the canvas content to the target format with the specified quality.
// This is the step that performs the data compression.
const compressedDataUrl = await canvas.convertToBlob({ type: format, quality: quality });
// A helper to convert blob to data URL since OffscreenCanvas.toDataURL is not standard yet
// on all execution contexts (like service workers).
const dataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(compressedDataUrl);
});
return { dataUrl, mimeType: format };
}

View File

@@ -0,0 +1,132 @@
class LRUNode<K, V> {
constructor(
public key: K,
public value: V,
public prev: LRUNode<K, V> | null = null,
public next: LRUNode<K, V> | null = null,
public frequency: number = 1,
public lastAccessed: number = Date.now(),
) {}
}
class LRUCache<K = string, V = any> {
private capacity: number;
private cache: Map<K, LRUNode<K, V>>;
private head: LRUNode<K, V>;
private tail: LRUNode<K, V>;
constructor(capacity: number) {
this.capacity = capacity > 0 ? capacity : 100;
this.cache = new Map<K, LRUNode<K, V>>();
this.head = new LRUNode<K, V>(null as any, null as any);
this.tail = new LRUNode<K, V>(null as any, null as any);
this.head.next = this.tail;
this.tail.prev = this.head;
}
private addToHead(node: LRUNode<K, V>): void {
node.prev = this.head;
node.next = this.head.next;
this.head.next!.prev = node;
this.head.next = node;
}
private removeNode(node: LRUNode<K, V>): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}
private moveToHead(node: LRUNode<K, V>): void {
this.removeNode(node);
this.addToHead(node);
}
private findVictimNode(): LRUNode<K, V> {
let victim = this.tail.prev!;
let minScore = this.calculateEvictionScore(victim);
let current = this.tail.prev;
let count = 0;
const maxCheck = Math.min(5, this.cache.size);
while (current && current !== this.head && count < maxCheck) {
const score = this.calculateEvictionScore(current);
if (score < minScore) {
minScore = score;
victim = current;
}
current = current.prev;
count++;
}
return victim;
}
private calculateEvictionScore(node: LRUNode<K, V>): number {
const now = Date.now();
const timeSinceAccess = now - node.lastAccessed;
const timeWeight = 1 / (1 + timeSinceAccess / (1000 * 60));
const frequencyWeight = Math.log(node.frequency + 1);
return frequencyWeight * timeWeight;
}
get(key: K): V | null {
const node = this.cache.get(key);
if (node) {
node.frequency++;
node.lastAccessed = Date.now();
this.moveToHead(node);
return node.value;
}
return null;
}
set(key: K, value: V): void {
const existingNode = this.cache.get(key);
if (existingNode) {
existingNode.value = value;
this.moveToHead(existingNode);
} else {
const newNode = new LRUNode(key, value);
if (this.cache.size >= this.capacity) {
const victimNode = this.findVictimNode();
this.removeNode(victimNode);
this.cache.delete(victimNode.key);
}
this.cache.set(key, newNode);
this.addToHead(newNode);
}
}
has(key: K): boolean {
return this.cache.has(key);
}
clear(): void {
this.cache.clear();
this.head.next = this.tail;
this.tail.prev = this.head;
}
get size(): number {
return this.cache.size;
}
/**
* Get cache statistics
*/
getStats(): { size: number; capacity: number; usage: number } {
return {
size: this.cache.size,
capacity: this.capacity,
usage: this.cache.size / this.capacity,
};
}
}
export default LRUCache;

View File

@@ -0,0 +1,369 @@
/**
* Model Cache Manager
*/
const CACHE_NAME = 'onnx-model-cache-v1';
const CACHE_EXPIRY_DAYS = 30;
const MAX_CACHE_SIZE_MB = 500;
export interface CacheMetadata {
timestamp: number;
modelUrl: string;
size: number;
version: string;
}
export interface CacheEntry {
url: string;
size: number;
sizeMB: number;
timestamp: number;
age: string;
expired: boolean;
}
export interface CacheStats {
totalSize: number;
totalSizeMB: number;
entryCount: number;
entries: CacheEntry[];
}
interface CacheEntryDetails {
url: string;
timestamp: number;
size: number;
}
export class ModelCacheManager {
private static instance: ModelCacheManager | null = null;
public static getInstance(): ModelCacheManager {
if (!ModelCacheManager.instance) {
ModelCacheManager.instance = new ModelCacheManager();
}
return ModelCacheManager.instance;
}
private constructor() {}
private getCacheMetadataKey(modelUrl: string): string {
const encodedUrl = encodeURIComponent(modelUrl);
return `https://cache-metadata.local/${encodedUrl}`;
}
private isCacheExpired(metadata: CacheMetadata): boolean {
const now = Date.now();
const expiryTime = metadata.timestamp + CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
return now > expiryTime;
}
private isMetadataUrl(url: string): boolean {
return url.startsWith('https://cache-metadata.local/');
}
private async collectCacheEntries(): Promise<{
entries: CacheEntryDetails[];
totalSize: number;
entryCount: number;
}> {
const cache = await caches.open(CACHE_NAME);
const keys = await cache.keys();
const entries: CacheEntryDetails[] = [];
let totalSize = 0;
let entryCount = 0;
for (const request of keys) {
if (this.isMetadataUrl(request.url)) continue;
const response = await cache.match(request);
if (response) {
const blob = await response.blob();
const size = blob.size;
totalSize += size;
entryCount++;
const metadataResponse = await cache.match(this.getCacheMetadataKey(request.url));
let timestamp = 0;
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
timestamp = metadata.timestamp;
} catch (error) {
console.warn('Failed to parse cache metadata:', error);
}
}
entries.push({
url: request.url,
timestamp,
size,
});
}
}
return { entries, totalSize, entryCount };
}
public async cleanupCacheOnDemand(newDataSize: number = 0): Promise<void> {
const cache = await caches.open(CACHE_NAME);
const { entries, totalSize } = await this.collectCacheEntries();
const maxSizeBytes = MAX_CACHE_SIZE_MB * 1024 * 1024;
const projectedSize = totalSize + newDataSize;
if (projectedSize <= maxSizeBytes) {
return;
}
console.log(
`Cache size (${(totalSize / 1024 / 1024).toFixed(2)}MB) + new data (${(newDataSize / 1024 / 1024).toFixed(2)}MB) exceeds limit (${MAX_CACHE_SIZE_MB}MB), cleaning up...`,
);
const expiredEntries: CacheEntryDetails[] = [];
const validEntries: CacheEntryDetails[] = [];
for (const entry of entries) {
const metadataResponse = await cache.match(this.getCacheMetadataKey(entry.url));
let isExpired = false;
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
isExpired = this.isCacheExpired(metadata);
} catch (error) {
isExpired = true;
}
} else {
isExpired = true;
}
if (isExpired) {
expiredEntries.push(entry);
} else {
validEntries.push(entry);
}
}
let currentSize = totalSize;
for (const entry of expiredEntries) {
await cache.delete(entry.url);
await cache.delete(this.getCacheMetadataKey(entry.url));
currentSize -= entry.size;
console.log(
`Cleaned up expired cache entry: ${entry.url} (${(entry.size / 1024 / 1024).toFixed(2)}MB)`,
);
}
if (currentSize + newDataSize > maxSizeBytes) {
validEntries.sort((a, b) => a.timestamp - b.timestamp);
for (const entry of validEntries) {
if (currentSize + newDataSize <= maxSizeBytes) break;
await cache.delete(entry.url);
await cache.delete(this.getCacheMetadataKey(entry.url));
currentSize -= entry.size;
console.log(
`Cleaned up old cache entry: ${entry.url} (${(entry.size / 1024 / 1024).toFixed(2)}MB)`,
);
}
}
console.log(`Cache cleanup complete. New size: ${(currentSize / 1024 / 1024).toFixed(2)}MB`);
}
public async storeCacheMetadata(modelUrl: string, size: number): Promise<void> {
const cache = await caches.open(CACHE_NAME);
const metadata: CacheMetadata = {
timestamp: Date.now(),
modelUrl,
size,
version: CACHE_NAME,
};
const metadataResponse = new Response(JSON.stringify(metadata), {
headers: { 'Content-Type': 'application/json' },
});
await cache.put(this.getCacheMetadataKey(modelUrl), metadataResponse);
}
public async getCachedModelData(modelUrl: string): Promise<ArrayBuffer | null> {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(modelUrl);
if (!cachedResponse) {
return null;
}
const metadataResponse = await cache.match(this.getCacheMetadataKey(modelUrl));
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
if (!this.isCacheExpired(metadata)) {
console.log('Model found in cache and not expired. Loading from cache.');
return cachedResponse.arrayBuffer();
} else {
console.log('Cached model is expired, removing...');
await this.deleteCacheEntry(modelUrl);
return null;
}
} catch (error) {
console.warn('Failed to parse cache metadata, treating as expired:', error);
await this.deleteCacheEntry(modelUrl);
return null;
}
} else {
console.log('Cached model has no metadata, treating as expired...');
await this.deleteCacheEntry(modelUrl);
return null;
}
}
public async storeModelData(modelUrl: string, data: ArrayBuffer): Promise<void> {
await this.cleanupCacheOnDemand(data.byteLength);
const cache = await caches.open(CACHE_NAME);
const response = new Response(data);
await cache.put(modelUrl, response);
await this.storeCacheMetadata(modelUrl, data.byteLength);
console.log(
`Model cached successfully (${(data.byteLength / 1024 / 1024).toFixed(2)}MB): ${modelUrl}`,
);
}
public async deleteCacheEntry(modelUrl: string): Promise<void> {
const cache = await caches.open(CACHE_NAME);
await cache.delete(modelUrl);
await cache.delete(this.getCacheMetadataKey(modelUrl));
}
public async clearAllCache(): Promise<void> {
const cache = await caches.open(CACHE_NAME);
const keys = await cache.keys();
for (const request of keys) {
await cache.delete(request);
}
console.log('All model cache entries cleared');
}
public async getCacheStats(): Promise<CacheStats> {
const { entries, totalSize, entryCount } = await this.collectCacheEntries();
const cache = await caches.open(CACHE_NAME);
const cacheEntries: CacheEntry[] = [];
for (const entry of entries) {
const metadataResponse = await cache.match(this.getCacheMetadataKey(entry.url));
let expired = false;
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
expired = this.isCacheExpired(metadata);
} catch (error) {
expired = true;
}
} else {
expired = true;
}
const age =
entry.timestamp > 0
? `${Math.round((Date.now() - entry.timestamp) / (1000 * 60 * 60 * 24))} days`
: 'unknown';
cacheEntries.push({
url: entry.url,
size: entry.size,
sizeMB: Number((entry.size / 1024 / 1024).toFixed(2)),
timestamp: entry.timestamp,
age,
expired,
});
}
return {
totalSize,
totalSizeMB: Number((totalSize / 1024 / 1024).toFixed(2)),
entryCount,
entries: cacheEntries.sort((a, b) => b.timestamp - a.timestamp),
};
}
public async manualCleanup(): Promise<void> {
await this.cleanupCacheOnDemand(0);
console.log('Manual cache cleanup completed');
}
/**
* Check if a specific model is cached and not expired
* @param modelUrl The model URL to check
* @returns Promise<boolean> True if model is cached and valid
*/
public async isModelCached(modelUrl: string): Promise<boolean> {
try {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(modelUrl);
if (!cachedResponse) {
return false;
}
const metadataResponse = await cache.match(this.getCacheMetadataKey(modelUrl));
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
return !this.isCacheExpired(metadata);
} catch (error) {
console.warn('Failed to parse cache metadata for cache check:', error);
return false;
}
} else {
// No metadata means expired
return false;
}
} catch (error) {
console.error('Error checking model cache:', error);
return false;
}
}
/**
* Check if any valid (non-expired) model cache exists
* @returns Promise<boolean> True if at least one valid model cache exists
*/
public async hasAnyValidCache(): Promise<boolean> {
try {
const cache = await caches.open(CACHE_NAME);
const keys = await cache.keys();
for (const request of keys) {
if (this.isMetadataUrl(request.url)) continue;
const metadataResponse = await cache.match(this.getCacheMetadataKey(request.url));
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
if (!this.isCacheExpired(metadata)) {
return true; // Found at least one valid cache
}
} catch (error) {
// Skip invalid metadata
continue;
}
}
}
return false;
} catch (error) {
console.error('Error checking for valid cache:', error);
return false;
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* Offscreen Document manager
* Ensures only one offscreen document is created across the entire extension to avoid conflicts
*/
export class OffscreenManager {
private static instance: OffscreenManager | null = null;
private isCreated = false;
private isCreating = false;
private createPromise: Promise<void> | null = null;
private constructor() {}
/**
* Get singleton instance
*/
public static getInstance(): OffscreenManager {
if (!OffscreenManager.instance) {
OffscreenManager.instance = new OffscreenManager();
}
return OffscreenManager.instance;
}
/**
* Ensure offscreen document exists
*/
public async ensureOffscreenDocument(): Promise<void> {
if (this.isCreated) {
return;
}
if (this.isCreating && this.createPromise) {
return this.createPromise;
}
this.isCreating = true;
this.createPromise = this._doCreateOffscreenDocument().finally(() => {
this.isCreating = false;
});
return this.createPromise;
}
private async _doCreateOffscreenDocument(): Promise<void> {
try {
if (!chrome.offscreen) {
throw new Error('Offscreen API not available. Chrome 109+ required.');
}
const existingContexts = await (chrome.runtime as any).getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
});
if (existingContexts && existingContexts.length > 0) {
console.log('OffscreenManager: Offscreen document already exists');
this.isCreated = true;
return;
}
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['WORKERS'],
justification: 'Need to run semantic similarity engine with workers',
});
this.isCreated = true;
console.log('OffscreenManager: Offscreen document created successfully');
} catch (error) {
console.error('OffscreenManager: Failed to create offscreen document:', error);
this.isCreated = false;
throw error;
}
}
/**
* Check if offscreen document is created
*/
public isOffscreenDocumentCreated(): boolean {
return this.isCreated;
}
/**
* Close offscreen document
*/
public async closeOffscreenDocument(): Promise<void> {
try {
if (chrome.offscreen && this.isCreated) {
await chrome.offscreen.closeDocument();
this.isCreated = false;
console.log('OffscreenManager: Offscreen document closed');
}
} catch (error) {
console.error('OffscreenManager: Failed to close offscreen document:', error);
}
}
/**
* Reset state (for testing)
*/
public reset(): void {
this.isCreated = false;
this.isCreating = false;
this.createPromise = null;
}
}
export const offscreenManager = OffscreenManager.getInstance();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,496 @@
/**
* 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;
}
}

View File

@@ -0,0 +1,264 @@
/**
* Text chunking utility
* Based on semantic chunking strategy, splits long text into small chunks suitable for vectorization
*/
export interface TextChunk {
text: string;
source: string;
index: number;
wordCount: number;
}
export interface ChunkingOptions {
maxWordsPerChunk?: number;
overlapSentences?: number;
minChunkLength?: number;
includeTitle?: boolean;
}
export class TextChunker {
private readonly defaultOptions: Required<ChunkingOptions> = {
maxWordsPerChunk: 80,
overlapSentences: 1,
minChunkLength: 20,
includeTitle: true,
};
public chunkText(content: string, title?: string, options?: ChunkingOptions): TextChunk[] {
const opts = { ...this.defaultOptions, ...options };
const chunks: TextChunk[] = [];
if (opts.includeTitle && title?.trim() && title.trim().length > 5) {
chunks.push({
text: title.trim(),
source: 'title',
index: 0,
wordCount: title.trim().split(/\s+/).length,
});
}
const cleanContent = content.trim();
if (!cleanContent) {
return chunks;
}
const sentences = this.splitIntoSentences(cleanContent);
if (sentences.length === 0) {
return this.fallbackChunking(cleanContent, chunks, opts);
}
const hasLongSentences = sentences.some(
(s: string) => s.split(/\s+/).length > opts.maxWordsPerChunk,
);
if (hasLongSentences) {
return this.mixedChunking(sentences, chunks, opts);
}
return this.groupSentencesIntoChunks(sentences, chunks, opts);
}
private splitIntoSentences(content: string): string[] {
const processedContent = content
.replace(/([。!?])\s*/g, '$1\n')
.replace(/([.!?])\s+(?=[A-Z])/g, '$1\n')
.replace(/([.!?]["'])\s+(?=[A-Z])/g, '$1\n')
.replace(/([.!?])\s*$/gm, '$1\n')
.replace(/([。!?][""])\s*/g, '$1\n')
.replace(/\n\s*\n/g, '\n');
const sentences = processedContent
.split('\n')
.map((s) => s.trim())
.filter((s) => s.length > 15);
if (sentences.length < 3 && content.length > 500) {
return this.aggressiveSentenceSplitting(content);
}
return sentences;
}
private aggressiveSentenceSplitting(content: string): string[] {
const sentences = content
.replace(/([.!?。!?])/g, '$1\n')
.replace(/([;:])/g, '$1\n')
.replace(/([)])\s*(?=[\u4e00-\u9fa5A-Z])/g, '$1\n')
.split('\n')
.map((s) => s.trim())
.filter((s) => s.length > 15);
const maxWordsPerChunk = 80;
const finalSentences: string[] = [];
for (const sentence of sentences) {
const words = sentence.split(/\s+/);
if (words.length <= maxWordsPerChunk) {
finalSentences.push(sentence);
} else {
const overlapWords = 5;
for (let i = 0; i < words.length; i += maxWordsPerChunk - overlapWords) {
const chunkWords = words.slice(i, i + maxWordsPerChunk);
const chunkText = chunkWords.join(' ');
if (chunkText.length > 15) {
finalSentences.push(chunkText);
}
}
}
}
return finalSentences;
}
/**
* Group sentences into chunks
*/
private groupSentencesIntoChunks(
sentences: string[],
existingChunks: TextChunk[],
options: Required<ChunkingOptions>,
): TextChunk[] {
const chunks = [...existingChunks];
let chunkIndex = chunks.length;
let i = 0;
while (i < sentences.length) {
let currentChunkText = '';
let currentWordCount = 0;
let sentencesUsed = 0;
while (i + sentencesUsed < sentences.length && currentWordCount < options.maxWordsPerChunk) {
const sentence = sentences[i + sentencesUsed];
const sentenceWords = sentence.split(/\s+/).length;
if (currentWordCount + sentenceWords > options.maxWordsPerChunk && currentWordCount > 0) {
break;
}
currentChunkText += (currentChunkText ? ' ' : '') + sentence;
currentWordCount += sentenceWords;
sentencesUsed++;
}
if (currentChunkText.trim().length > options.minChunkLength) {
chunks.push({
text: currentChunkText.trim(),
source: `content_chunk_${chunkIndex}`,
index: chunkIndex,
wordCount: currentWordCount,
});
chunkIndex++;
}
i += Math.max(1, sentencesUsed - options.overlapSentences);
}
return chunks;
}
/**
* Mixed chunking method (handles long sentences)
*/
private mixedChunking(
sentences: string[],
existingChunks: TextChunk[],
options: Required<ChunkingOptions>,
): TextChunk[] {
const chunks = [...existingChunks];
let chunkIndex = chunks.length;
for (const sentence of sentences) {
const sentenceWords = sentence.split(/\s+/).length;
if (sentenceWords <= options.maxWordsPerChunk) {
chunks.push({
text: sentence.trim(),
source: `sentence_chunk_${chunkIndex}`,
index: chunkIndex,
wordCount: sentenceWords,
});
chunkIndex++;
} else {
const words = sentence.split(/\s+/);
for (let i = 0; i < words.length; i += options.maxWordsPerChunk) {
const chunkWords = words.slice(i, i + options.maxWordsPerChunk);
const chunkText = chunkWords.join(' ');
if (chunkText.length > options.minChunkLength) {
chunks.push({
text: chunkText,
source: `long_sentence_chunk_${chunkIndex}_part_${Math.floor(i / options.maxWordsPerChunk)}`,
index: chunkIndex,
wordCount: chunkWords.length,
});
}
}
chunkIndex++;
}
}
return chunks;
}
/**
* Fallback chunking (when sentence splitting fails)
*/
private fallbackChunking(
content: string,
existingChunks: TextChunk[],
options: Required<ChunkingOptions>,
): TextChunk[] {
const chunks = [...existingChunks];
let chunkIndex = chunks.length;
const paragraphs = content
.split(/\n\s*\n/)
.filter((p) => p.trim().length > options.minChunkLength);
if (paragraphs.length > 1) {
paragraphs.forEach((paragraph, index) => {
const cleanParagraph = paragraph.trim();
if (cleanParagraph.length > 0) {
const words = cleanParagraph.split(/\s+/);
const maxWordsPerChunk = 150;
for (let i = 0; i < words.length; i += maxWordsPerChunk) {
const chunkWords = words.slice(i, i + maxWordsPerChunk);
const chunkText = chunkWords.join(' ');
if (chunkText.length > options.minChunkLength) {
chunks.push({
text: chunkText,
source: `paragraph_${index}_chunk_${Math.floor(i / maxWordsPerChunk)}`,
index: chunkIndex,
wordCount: chunkWords.length,
});
chunkIndex++;
}
}
}
});
} else {
const words = content.trim().split(/\s+/);
const maxWordsPerChunk = 150;
for (let i = 0; i < words.length; i += maxWordsPerChunk) {
const chunkWords = words.slice(i, i + maxWordsPerChunk);
const chunkText = chunkWords.join(' ');
if (chunkText.length > options.minChunkLength) {
chunks.push({
text: chunkText,
source: `content_chunk_${Math.floor(i / maxWordsPerChunk)}`,
index: chunkIndex,
wordCount: chunkWords.length,
});
chunkIndex++;
}
}
}
return chunks;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
var ortWasmThreaded = (() => {
var _scriptName = import.meta.url;
return (
async function(moduleArg = {}) {
var moduleRtn;
var e=moduleArg,aa,ca,da=new Promise((a,b)=>{aa=a;ca=b}),ea="object"==typeof window,k="undefined"!=typeof WorkerGlobalScope,n="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node&&"renderer"!=process.type,q=k&&self.name?.startsWith("em-pthread");if(n){const {createRequire:a}=await import("module");var require=a(import.meta.url),fa=require("worker_threads");global.Worker=fa.Worker;q=(k=!fa.pc)&&"em-pthread"==fa.workerData}
e.mountExternalData=(a,b)=>{a.startsWith("./")&&(a=a.substring(2));(e.Fb||(e.Fb=new Map)).set(a,b)};e.unmountExternalData=()=>{delete e.Fb};var SharedArrayBuffer=globalThis.SharedArrayBuffer??(new WebAssembly.Memory({initial:0,maximum:0,qc:!0})).buffer.constructor;
const ha=a=>async(...b)=>{try{if(e.Gb)throw Error("Session already started");const c=e.Gb={ec:b[0],errors:[]},d=await a(...b);if(e.Gb!==c)throw Error("Session mismatch");e.Kb?.flush();const f=c.errors;if(0<f.length){let g=await Promise.all(f);g=g.filter(h=>h);if(0<g.length)throw Error(g.join("\n"));}return d}finally{e.Gb=null}};
e.jsepInit=(a,b)=>{if("webgpu"===a){[e.Kb,e.Vb,e.Zb,e.Lb,e.Yb,e.kb,e.$b,e.bc,e.Wb,e.Xb,e.ac]=b;const c=e.Kb;e.jsepRegisterBuffer=(d,f,g,h)=>c.registerBuffer(d,f,g,h);e.jsepGetBuffer=d=>c.getBuffer(d);e.jsepCreateDownloader=(d,f,g)=>c.createDownloader(d,f,g);e.jsepOnCreateSession=d=>{c.onCreateSession(d)};e.jsepOnReleaseSession=d=>{c.onReleaseSession(d)};e.jsepOnRunStart=d=>c.onRunStart(d);e.cc=(d,f)=>{c.upload(d,f)}}else if("webnn"===a){const c=b[0];[e.oc,e.Ob,e.webnnEnsureTensor,e.Pb,e.webnnDownloadTensor]=
b.slice(1);e.webnnReleaseTensorId=e.Ob;e.webnnUploadTensor=e.Pb;e.webnnOnRunStart=d=>c.onRunStart(d);e.webnnOnRunEnd=c.onRunEnd.bind(c);e.webnnRegisterMLContext=(d,f)=>{c.registerMLContext(d,f)};e.webnnOnReleaseSession=d=>{c.onReleaseSession(d)};e.webnnCreateMLTensorDownloader=(d,f)=>c.createMLTensorDownloader(d,f);e.webnnRegisterMLTensor=(d,f,g,h)=>c.registerMLTensor(d,f,g,h);e.webnnCreateMLContext=d=>c.createMLContext(d);e.webnnRegisterMLConstant=(d,f,g,h,l,m)=>c.registerMLConstant(d,f,g,h,l,e.Fb,
m);e.webnnRegisterGraphInput=c.registerGraphInput.bind(c);e.webnnIsGraphInput=c.isGraphInput.bind(c);e.webnnRegisterGraphOutput=c.registerGraphOutput.bind(c);e.webnnIsGraphOutput=c.isGraphOutput.bind(c);e.webnnCreateTemporaryTensor=c.createTemporaryTensor.bind(c);e.webnnIsGraphInputOutputTypeSupported=c.isGraphInputOutputTypeSupported.bind(c)}};
let ja=()=>{const a=(b,c,d)=>(...f)=>{const g=t,h=c?.();f=b(...f);const l=c?.();h!==l&&(b=l,d(h),c=d=null);return t!=g?ia():f};(b=>{for(const c of b)e[c]=a(e[c],()=>e[c],d=>e[c]=d)})(["_OrtAppendExecutionProvider","_OrtCreateSession","_OrtRun","_OrtRunWithBinding","_OrtBindInput"]);"undefined"!==typeof ha&&(e._OrtRun=ha(e._OrtRun),e._OrtRunWithBinding=ha(e._OrtRunWithBinding));ja=void 0};e.asyncInit=()=>{ja?.()};var ka=Object.assign({},e),la="./this.program",ma=(a,b)=>{throw b;},v="",na,oa;
if(n){var fs=require("fs"),pa=require("path");import.meta.url.startsWith("data:")||(v=pa.dirname(require("url").fileURLToPath(import.meta.url))+"/");oa=a=>{a=qa(a)?new URL(a):a;return fs.readFileSync(a)};na=async a=>{a=qa(a)?new URL(a):a;return fs.readFileSync(a,void 0)};!e.thisProgram&&1<process.argv.length&&(la=process.argv[1].replace(/\\/g,"/"));process.argv.slice(2);ma=(a,b)=>{process.exitCode=a;throw b;}}else if(ea||k)k?v=self.location.href:"undefined"!=typeof document&&
document.currentScript&&(v=document.currentScript.src),_scriptName&&(v=_scriptName),v.startsWith("blob:")?v="":v=v.slice(0,v.replace(/[?#].*/,"").lastIndexOf("/")+1),n||(k&&(oa=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=async a=>{if(qa(a))return new Promise((c,d)=>{var f=new XMLHttpRequest;f.open("GET",a,!0);f.responseType="arraybuffer";f.onload=()=>{200==f.status||0==f.status&&f.response?c(f.response):d(f.status)};
f.onerror=d;f.send(null)});var b=await fetch(a,{credentials:"same-origin"});if(b.ok)return b.arrayBuffer();throw Error(b.status+" : "+b.url);});var ra=console.log.bind(console),sa=console.error.bind(console);n&&(ra=(...a)=>fs.writeSync(1,a.join(" ")+"\n"),sa=(...a)=>fs.writeSync(2,a.join(" ")+"\n"));var ta=ra,x=sa;Object.assign(e,ka);ka=null;var ua=e.wasmBinary,z,va,A=!1,wa,B,xa,ya,za,Aa,Ba,Ca,C,Da,Ea,qa=a=>a.startsWith("file://");function D(){z.buffer!=B.buffer&&E();return B}
function F(){z.buffer!=B.buffer&&E();return xa}function G(){z.buffer!=B.buffer&&E();return ya}function Fa(){z.buffer!=B.buffer&&E();return za}function H(){z.buffer!=B.buffer&&E();return Aa}function I(){z.buffer!=B.buffer&&E();return Ba}function Ga(){z.buffer!=B.buffer&&E();return Ca}function J(){z.buffer!=B.buffer&&E();return Ea}
if(q){var Ha;if(n){var Ia=fa.parentPort;Ia.on("message",b=>onmessage({data:b}));Object.assign(globalThis,{self:global,postMessage:b=>Ia.postMessage(b)})}var Ja=!1;x=function(...b){b=b.join(" ");n?fs.writeSync(2,b+"\n"):console.error(b)};self.alert=function(...b){postMessage({Cb:"alert",text:b.join(" "),jc:Ka()})};self.onunhandledrejection=b=>{throw b.reason||b;};function a(b){try{var c=b.data,d=c.Cb;if("load"===d){let f=[];self.onmessage=g=>f.push(g);self.startWorker=()=>{postMessage({Cb:"loaded"});
for(let g of f)a(g);self.onmessage=a};for(const g of c.Sb)if(!e[g]||e[g].proxy)e[g]=(...h)=>{postMessage({Cb:"callHandler",Rb:g,args:h})},"print"==g&&(ta=e[g]),"printErr"==g&&(x=e[g]);z=c.lc;E();Ha(c.mc)}else if("run"===d){La(c.Bb);Ma(c.Bb,0,0,1,0,0);Na();Oa(c.Bb);Ja||(Pa(),Ja=!0);try{Qa(c.hc,c.Ib)}catch(f){if("unwind"!=f)throw f;}}else"setimmediate"!==c.target&&("checkMailbox"===d?Ja&&Ra():d&&(x(`worker: received unknown command ${d}`),x(c)))}catch(f){throw Sa(),f;}}self.onmessage=a}
function E(){var a=z.buffer;e.HEAP8=B=new Int8Array(a);e.HEAP16=ya=new Int16Array(a);e.HEAPU8=xa=new Uint8Array(a);e.HEAPU16=za=new Uint16Array(a);e.HEAP32=Aa=new Int32Array(a);e.HEAPU32=Ba=new Uint32Array(a);e.HEAPF32=Ca=new Float32Array(a);e.HEAPF64=Ea=new Float64Array(a);e.HEAP64=C=new BigInt64Array(a);e.HEAPU64=Da=new BigUint64Array(a)}q||(z=new WebAssembly.Memory({initial:256,maximum:65536,shared:!0}),E());function Ta(){q?startWorker(e):K.Da()}var Ua=0,Va=null;
function Wa(){Ua--;if(0==Ua&&Va){var a=Va;Va=null;a()}}function L(a){a="Aborted("+a+")";x(a);A=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");ca(a);throw a;}var Xa;async function Ya(a){if(!ua)try{var b=await na(a);return new Uint8Array(b)}catch{}if(a==Xa&&ua)a=new Uint8Array(ua);else if(oa)a=oa(a);else throw"both async and sync fetching of the wasm failed";return a}
async function Za(a,b){try{var c=await Ya(a);return await WebAssembly.instantiate(c,b)}catch(d){x(`failed to asynchronously prepare wasm: ${d}`),L(d)}}async function $a(a){var b=Xa;if(!ua&&"function"==typeof WebAssembly.instantiateStreaming&&!qa(b)&&!n)try{var c=fetch(b,{credentials:"same-origin"});return await WebAssembly.instantiateStreaming(c,a)}catch(d){x(`wasm streaming compile failed: ${d}`),x("falling back to ArrayBuffer instantiation")}return Za(b,a)}
function ab(){bb={L:cb,Aa:db,b:eb,$:fb,A:gb,pa:hb,X:ib,Z:jb,qa:kb,na:lb,ga:mb,ma:nb,J:ob,Y:pb,V:qb,oa:rb,W:sb,va:tb,E:ub,Q:vb,O:wb,D:xb,v:yb,r:zb,P:Ab,z:Bb,R:Cb,ja:Db,T:Eb,aa:Fb,M:Gb,F:Hb,ia:Oa,sa:Ib,t:Jb,Ca:Kb,w:Lb,o:Mb,m:Nb,c:Ob,Ba:Pb,n:Qb,j:Rb,u:Sb,p:Tb,f:Ub,s:Vb,l:Wb,e:Xb,k:Yb,h:Zb,g:$b,d:ac,da:bc,ea:cc,fa:dc,ba:ec,ca:fc,N:gc,xa:hc,ua:ic,i:jc,C:kc,G:lc,ta:mc,x:nc,ra:oc,U:pc,q:qc,y:rc,K:sc,S:tc,za:uc,ya:vc,ka:wc,la:xc,_:yc,B:zc,I:Ac,ha:Bc,H:Cc,a:z,wa:Dc};return{a:bb}}
var Ec={840156:(a,b,c,d,f)=>{if("undefined"==typeof e||!e.Fb)return 1;a=M(Number(a>>>0));a.startsWith("./")&&(a=a.substring(2));a=e.Fb.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(f){case 0:F().set(g,d>>>0);break;case 1:e.nc?e.nc(d,g):e.cc(d,g);break;default:return 4}return 0}catch{return 4}},840980:(a,b,c)=>{e.Pb(a,F().subarray(b>>>0,b+c>>>0))},841044:()=>e.oc(),841086:a=>{e.Ob(a)},841123:()=>{e.Wb()},841154:()=>
{e.Xb()},841183:()=>{e.ac()},841208:a=>e.Vb(a),841241:a=>e.Zb(a),841273:(a,b,c)=>{e.Lb(Number(a),Number(b),Number(c),!0)},841336:(a,b,c)=>{e.Lb(Number(a),Number(b),Number(c))},841393:()=>"undefined"!==typeof wasmOffsetConverter,841450:a=>{e.kb("Abs",a,void 0)},841501:a=>{e.kb("Neg",a,void 0)},841552:a=>{e.kb("Floor",a,void 0)},841605:a=>{e.kb("Ceil",a,void 0)},841657:a=>{e.kb("Reciprocal",a,void 0)},841715:a=>{e.kb("Sqrt",a,void 0)},841767:a=>{e.kb("Exp",a,void 0)},841818:a=>{e.kb("Erf",a,void 0)},
841869:a=>{e.kb("Sigmoid",a,void 0)},841924:(a,b,c)=>{e.kb("HardSigmoid",a,{alpha:b,beta:c})},842003:a=>{e.kb("Log",a,void 0)},842054:a=>{e.kb("Sin",a,void 0)},842105:a=>{e.kb("Cos",a,void 0)},842156:a=>{e.kb("Tan",a,void 0)},842207:a=>{e.kb("Asin",a,void 0)},842259:a=>{e.kb("Acos",a,void 0)},842311:a=>{e.kb("Atan",a,void 0)},842363:a=>{e.kb("Sinh",a,void 0)},842415:a=>{e.kb("Cosh",a,void 0)},842467:a=>{e.kb("Asinh",a,void 0)},842520:a=>{e.kb("Acosh",a,void 0)},842573:a=>{e.kb("Atanh",a,void 0)},
842626:a=>{e.kb("Tanh",a,void 0)},842678:a=>{e.kb("Not",a,void 0)},842729:(a,b,c)=>{e.kb("Clip",a,{min:b,max:c})},842798:a=>{e.kb("Clip",a,void 0)},842850:(a,b)=>{e.kb("Elu",a,{alpha:b})},842908:a=>{e.kb("Gelu",a,void 0)},842960:a=>{e.kb("Relu",a,void 0)},843012:(a,b)=>{e.kb("LeakyRelu",a,{alpha:b})},843076:(a,b)=>{e.kb("ThresholdedRelu",a,{alpha:b})},843146:(a,b)=>{e.kb("Cast",a,{to:b})},843204:a=>{e.kb("Add",a,void 0)},843255:a=>{e.kb("Sub",a,void 0)},843306:a=>{e.kb("Mul",a,void 0)},843357:a=>
{e.kb("Div",a,void 0)},843408:a=>{e.kb("Pow",a,void 0)},843459:a=>{e.kb("Equal",a,void 0)},843512:a=>{e.kb("Greater",a,void 0)},843567:a=>{e.kb("GreaterOrEqual",a,void 0)},843629:a=>{e.kb("Less",a,void 0)},843681:a=>{e.kb("LessOrEqual",a,void 0)},843740:(a,b,c,d,f)=>{e.kb("ReduceMean",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},843915:(a,b,c,d,f)=>{e.kb("ReduceMax",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>
0,Number(f)>>>0)):[]})},844089:(a,b,c,d,f)=>{e.kb("ReduceMin",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},844263:(a,b,c,d,f)=>{e.kb("ReduceProd",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},844438:(a,b,c,d,f)=>{e.kb("ReduceSum",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},844612:(a,b,c,d,f)=>{e.kb("ReduceL1",a,{keepDims:!!b,
noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},844785:(a,b,c,d,f)=>{e.kb("ReduceL2",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},844958:(a,b,c,d,f)=>{e.kb("ReduceLogSum",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},845135:(a,b,c,d,f)=>{e.kb("ReduceSumSquare",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>
0,Number(f)>>>0)):[]})},845315:(a,b,c,d,f)=>{e.kb("ReduceLogSumExp",a,{keepDims:!!b,noopWithEmptyAxes:!!c,axes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},845495:a=>{e.kb("Where",a,void 0)},845548:(a,b,c)=>{e.kb("Transpose",a,{perm:b?Array.from(H().subarray(Number(b)>>>0,Number(c)>>>0)):[]})},845672:(a,b,c,d)=>{e.kb("DepthToSpace",a,{blocksize:b,mode:M(c),format:d?"NHWC":"NCHW"})},845805:(a,b,c,d)=>{e.kb("DepthToSpace",a,{blocksize:b,mode:M(c),format:d?"NHWC":"NCHW"})},845938:(a,
b,c,d,f,g,h,l,m,p,r,u,w,y,ba)=>{e.kb("ConvTranspose",a,{format:m?"NHWC":"NCHW",autoPad:b,dilations:[c],group:d,kernelShape:[f],pads:[g,h],strides:[l],wIsConst:()=>!!D()[p>>>0],outputPadding:r?Array.from(H().subarray(Number(r)>>>0,Number(u)>>>0)):[],outputShape:w?Array.from(H().subarray(Number(w)>>>0,Number(y)>>>0)):[],activation:M(ba)})},846371:(a,b,c,d,f,g,h,l,m,p,r,u,w,y)=>{e.kb("ConvTranspose",a,{format:l?"NHWC":"NCHW",autoPad:b,dilations:Array.from(H().subarray(Number(c)>>>0,(Number(c)>>>0)+2>>>
0)),group:d,kernelShape:Array.from(H().subarray(Number(f)>>>0,(Number(f)>>>0)+2>>>0)),pads:Array.from(H().subarray(Number(g)>>>0,(Number(g)>>>0)+4>>>0)),strides:Array.from(H().subarray(Number(h)>>>0,(Number(h)>>>0)+2>>>0)),wIsConst:()=>!!D()[m>>>0],outputPadding:p?Array.from(H().subarray(Number(p)>>>0,Number(r)>>>0)):[],outputShape:u?Array.from(H().subarray(Number(u)>>>0,Number(w)>>>0)):[],activation:M(y)})},847032:(a,b,c,d,f,g,h,l,m,p,r,u,w,y,ba)=>{e.kb("ConvTranspose",a,{format:m?"NHWC":"NCHW",
autoPad:b,dilations:[c],group:d,kernelShape:[f],pads:[g,h],strides:[l],wIsConst:()=>!!D()[p>>>0],outputPadding:r?Array.from(H().subarray(Number(r)>>>0,Number(u)>>>0)):[],outputShape:w?Array.from(H().subarray(Number(w)>>>0,Number(y)>>>0)):[],activation:M(ba)})},847465:(a,b,c,d,f,g,h,l,m,p,r,u,w,y)=>{e.kb("ConvTranspose",a,{format:l?"NHWC":"NCHW",autoPad:b,dilations:Array.from(H().subarray(Number(c)>>>0,(Number(c)>>>0)+2>>>0)),group:d,kernelShape:Array.from(H().subarray(Number(f)>>>0,(Number(f)>>>0)+
2>>>0)),pads:Array.from(H().subarray(Number(g)>>>0,(Number(g)>>>0)+4>>>0)),strides:Array.from(H().subarray(Number(h)>>>0,(Number(h)>>>0)+2>>>0)),wIsConst:()=>!!D()[m>>>0],outputPadding:p?Array.from(H().subarray(Number(p)>>>0,Number(r)>>>0)):[],outputShape:u?Array.from(H().subarray(Number(u)>>>0,Number(w)>>>0)):[],activation:M(y)})},848126:(a,b)=>{e.kb("GlobalAveragePool",a,{format:b?"NHWC":"NCHW"})},848217:(a,b,c,d,f,g,h,l,m,p,r,u,w,y)=>{e.kb("AveragePool",a,{format:y?"NHWC":"NCHW",auto_pad:b,ceil_mode:c,
count_include_pad:d,storage_order:f,dilations:g?Array.from(H().subarray(Number(g)>>>0,Number(h)>>>0)):[],kernel_shape:l?Array.from(H().subarray(Number(l)>>>0,Number(m)>>>0)):[],pads:p?Array.from(H().subarray(Number(p)>>>0,Number(r)>>>0)):[],strides:u?Array.from(H().subarray(Number(u)>>>0,Number(w)>>>0)):[]})},848696:(a,b)=>{e.kb("GlobalAveragePool",a,{format:b?"NHWC":"NCHW"})},848787:(a,b,c,d,f,g,h,l,m,p,r,u,w,y)=>{e.kb("AveragePool",a,{format:y?"NHWC":"NCHW",auto_pad:b,ceil_mode:c,count_include_pad:d,
storage_order:f,dilations:g?Array.from(H().subarray(Number(g)>>>0,Number(h)>>>0)):[],kernel_shape:l?Array.from(H().subarray(Number(l)>>>0,Number(m)>>>0)):[],pads:p?Array.from(H().subarray(Number(p)>>>0,Number(r)>>>0)):[],strides:u?Array.from(H().subarray(Number(u)>>>0,Number(w)>>>0)):[]})},849266:(a,b)=>{e.kb("GlobalMaxPool",a,{format:b?"NHWC":"NCHW"})},849353:(a,b,c,d,f,g,h,l,m,p,r,u,w,y)=>{e.kb("MaxPool",a,{format:y?"NHWC":"NCHW",auto_pad:b,ceil_mode:c,count_include_pad:d,storage_order:f,dilations:g?
Array.from(H().subarray(Number(g)>>>0,Number(h)>>>0)):[],kernel_shape:l?Array.from(H().subarray(Number(l)>>>0,Number(m)>>>0)):[],pads:p?Array.from(H().subarray(Number(p)>>>0,Number(r)>>>0)):[],strides:u?Array.from(H().subarray(Number(u)>>>0,Number(w)>>>0)):[]})},849828:(a,b)=>{e.kb("GlobalMaxPool",a,{format:b?"NHWC":"NCHW"})},849915:(a,b,c,d,f,g,h,l,m,p,r,u,w,y)=>{e.kb("MaxPool",a,{format:y?"NHWC":"NCHW",auto_pad:b,ceil_mode:c,count_include_pad:d,storage_order:f,dilations:g?Array.from(H().subarray(Number(g)>>>
0,Number(h)>>>0)):[],kernel_shape:l?Array.from(H().subarray(Number(l)>>>0,Number(m)>>>0)):[],pads:p?Array.from(H().subarray(Number(p)>>>0,Number(r)>>>0)):[],strides:u?Array.from(H().subarray(Number(u)>>>0,Number(w)>>>0)):[]})},850390:(a,b,c,d,f)=>{e.kb("Gemm",a,{alpha:b,beta:c,transA:d,transB:f})},850494:a=>{e.kb("MatMul",a,void 0)},850548:(a,b,c,d)=>{e.kb("ArgMax",a,{keepDims:!!b,selectLastIndex:!!c,axis:d})},850656:(a,b,c,d)=>{e.kb("ArgMin",a,{keepDims:!!b,selectLastIndex:!!c,axis:d})},850764:(a,
b)=>{e.kb("Softmax",a,{axis:b})},850827:(a,b)=>{e.kb("Concat",a,{axis:b})},850887:(a,b,c,d,f)=>{e.kb("Split",a,{axis:b,numOutputs:c,splitSizes:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},851043:a=>{e.kb("Expand",a,void 0)},851097:(a,b)=>{e.kb("Gather",a,{axis:Number(b)})},851168:(a,b)=>{e.kb("GatherElements",a,{axis:Number(b)})},851247:(a,b)=>{e.kb("GatherND",a,{batch_dims:Number(b)})},851326:(a,b,c,d,f,g,h,l,m,p,r)=>{e.kb("Resize",a,{antialias:b,axes:c?Array.from(H().subarray(Number(c)>>>
0,Number(d)>>>0)):[],coordinateTransformMode:M(f),cubicCoeffA:g,excludeOutside:h,extrapolationValue:l,keepAspectRatioPolicy:M(m),mode:M(p),nearestMode:M(r)})},851688:(a,b,c,d,f,g,h)=>{e.kb("Slice",a,{starts:b?Array.from(H().subarray(Number(b)>>>0,Number(c)>>>0)):[],ends:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[],axes:g?Array.from(H().subarray(Number(g)>>>0,Number(h)>>>0)):[]})},851952:a=>{e.kb("Tile",a,void 0)},852004:(a,b,c)=>{e.kb("InstanceNormalization",a,{epsilon:b,format:c?"NHWC":
"NCHW"})},852118:(a,b,c)=>{e.kb("InstanceNormalization",a,{epsilon:b,format:c?"NHWC":"NCHW"})},852232:a=>{e.kb("Range",a,void 0)},852285:(a,b)=>{e.kb("Einsum",a,{equation:M(b)})},852366:(a,b,c,d,f)=>{e.kb("Pad",a,{mode:b,value:c,pads:d?Array.from(H().subarray(Number(d)>>>0,Number(f)>>>0)):[]})},852509:(a,b,c,d,f,g)=>{e.kb("BatchNormalization",a,{epsilon:b,momentum:c,spatial:!!f,trainingMode:!!d,format:g?"NHWC":"NCHW"})},852678:(a,b,c,d,f,g)=>{e.kb("BatchNormalization",a,{epsilon:b,momentum:c,spatial:!!f,
trainingMode:!!d,format:g?"NHWC":"NCHW"})},852847:(a,b,c)=>{e.kb("CumSum",a,{exclusive:Number(b),reverse:Number(c)})},852944:(a,b,c)=>{e.kb("DequantizeLinear",a,{axis:b,blockSize:c})},853034:(a,b,c,d,f)=>{e.kb("GridSample",a,{align_corners:b,mode:M(c),padding_mode:M(d),format:f?"NHWC":"NCHW"})},853204:(a,b,c,d,f)=>{e.kb("GridSample",a,{align_corners:b,mode:M(c),padding_mode:M(d),format:f?"NHWC":"NCHW"})},853374:(a,b)=>{e.kb("ScatterND",a,{reduction:M(b)})},853459:(a,b,c,d,f,g,h,l,m)=>{e.kb("Attention",
a,{numHeads:b,isUnidirectional:c,maskFilterValue:d,scale:f,doRotary:g,qkvHiddenSizes:h?Array.from(H().subarray(Number(l)>>>0,Number(l)+h>>>0)):[],pastPresentShareBuffer:!!m})},853731:a=>{e.kb("BiasAdd",a,void 0)},853786:a=>{e.kb("BiasSplitGelu",a,void 0)},853847:a=>{e.kb("FastGelu",a,void 0)},853903:(a,b,c,d,f,g,h,l,m,p,r,u,w,y,ba,Wd)=>{e.kb("Conv",a,{format:u?"NHWC":"NCHW",auto_pad:b,dilations:c?Array.from(H().subarray(Number(c)>>>0,Number(d)>>>0)):[],group:f,kernel_shape:g?Array.from(H().subarray(Number(g)>>>
0,Number(h)>>>0)):[],pads:l?Array.from(H().subarray(Number(l)>>>0,Number(m)>>>0)):[],strides:p?Array.from(H().subarray(Number(p)>>>0,Number(r)>>>0)):[],w_is_const:()=>!!D()[Number(w)>>>0],activation:M(y),activation_params:ba?Array.from(Ga().subarray(Number(ba)>>>0,Number(Wd)>>>0)):[]})},854487:a=>{e.kb("Gelu",a,void 0)},854539:(a,b,c,d,f,g,h,l,m)=>{e.kb("GroupQueryAttention",a,{numHeads:b,kvNumHeads:c,scale:d,softcap:f,doRotary:g,rotaryInterleaved:h,smoothSoftmax:l,localWindowSize:m})},854756:(a,
b,c,d)=>{e.kb("LayerNormalization",a,{axis:b,epsilon:c,simplified:!!d})},854867:(a,b,c,d)=>{e.kb("LayerNormalization",a,{axis:b,epsilon:c,simplified:!!d})},854978:(a,b,c,d,f,g)=>{e.kb("MatMulNBits",a,{k:b,n:c,accuracyLevel:d,bits:f,blockSize:g})},855105:(a,b,c,d,f,g)=>{e.kb("MultiHeadAttention",a,{numHeads:b,isUnidirectional:c,maskFilterValue:d,scale:f,doRotary:g})},855264:(a,b)=>{e.kb("QuickGelu",a,{alpha:b})},855328:(a,b,c,d,f)=>{e.kb("RotaryEmbedding",a,{interleaved:!!b,numHeads:c,rotaryEmbeddingDim:d,
scale:f})},855467:(a,b,c)=>{e.kb("SkipLayerNormalization",a,{epsilon:b,simplified:!!c})},855569:(a,b,c)=>{e.kb("SkipLayerNormalization",a,{epsilon:b,simplified:!!c})},855671:(a,b,c,d)=>{e.kb("GatherBlockQuantized",a,{gatherAxis:b,quantizeAxis:c,blockSize:d})},855792:a=>{e.$b(a)},855826:(a,b)=>e.bc(Number(a),Number(b),e.Gb.ec,e.Gb.errors)};function db(a,b,c){return Fc(async()=>{await e.Yb(Number(a),Number(b),Number(c))})}function cb(){return"undefined"!==typeof wasmOffsetConverter}
class Gc{name="ExitStatus";constructor(a){this.message=`Program terminated with exit(${a})`;this.status=a}}
var Hc=a=>{a.terminate();a.onmessage=()=>{}},Ic=[],Mc=a=>{0==N.length&&(Jc(),Kc(N[0]));var b=N.pop();if(!b)return 6;Lc.push(b);O[a.Bb]=b;b.Bb=a.Bb;var c={Cb:"run",hc:a.fc,Ib:a.Ib,Bb:a.Bb};n&&b.unref();b.postMessage(c,a.Nb);return 0},P=0,Q=(a,b,...c)=>{for(var d=2*c.length,f=Nc(),g=Oc(8*d),h=g>>>3,l=0;l<c.length;l++){var m=c[l];"bigint"==typeof m?(C[h+2*l]=1n,C[h+2*l+1]=m):(C[h+2*l]=0n,J()[h+2*l+1>>>0]=m)}a=Pc(a,0,d,g,b);Qc(f);return a};
function Dc(a){if(q)return Q(0,1,a);wa=a;if(!(0<P)){for(var b of Lc)Hc(b);for(b of N)Hc(b);N=[];Lc=[];O={};A=!0}ma(a,new Gc(a))}function Rc(a){if(q)return Q(1,0,a);yc(a)}var yc=a=>{wa=a;if(q)throw Rc(a),"unwind";Dc(a)},N=[],Lc=[],Sc=[],O={};function Tc(){for(var a=e.numThreads-1;a--;)Jc();Ic.unshift(()=>{Ua++;Uc(()=>Wa())})}var Wc=a=>{var b=a.Bb;delete O[b];N.push(a);Lc.splice(Lc.indexOf(a),1);a.Bb=0;Vc(b)};function Na(){Sc.forEach(a=>a())}
var Kc=a=>new Promise(b=>{a.onmessage=g=>{g=g.data;var h=g.Cb;if(g.Hb&&g.Hb!=Ka()){var l=O[g.Hb];l?l.postMessage(g,g.Nb):x(`Internal error! Worker sent a message "${h}" to target pthread ${g.Hb}, but that thread no longer exists!`)}else if("checkMailbox"===h)Ra();else if("spawnThread"===h)Mc(g);else if("cleanupThread"===h)Wc(O[g.ic]);else if("loaded"===h)a.loaded=!0,n&&!a.Bb&&a.unref(),b(a);else if("alert"===h)alert(`Thread ${g.jc}: ${g.text}`);else if("setimmediate"===g.target)a.postMessage(g);else if("callHandler"===
h)e[g.Rb](...g.args);else h&&x(`worker sent an unknown command ${h}`)};a.onerror=g=>{x(`${"worker sent an error!"} ${g.filename}:${g.lineno}: ${g.message}`);throw g;};n&&(a.on("message",g=>a.onmessage({data:g})),a.on("error",g=>a.onerror(g)));var c=[],d=[],f;for(f of d)e.propertyIsEnumerable(f)&&c.push(f);a.postMessage({Cb:"load",Sb:c,lc:z,mc:va})});function Uc(a){q?a():Promise.all(N.map(Kc)).then(a)}
function Jc(){var a=new Worker(new URL(import.meta.url),{type:"module",workerData:"em-pthread",name:"em-pthread"});N.push(a)}var La=a=>{E();var b=I()[a+52>>>2>>>0];a=I()[a+56>>>2>>>0];Xc(b,b-a);Qc(b)},Qa=(a,b)=>{P=0;a=Yc(a,b);0<P?wa=a:Zc(a)};class $c{constructor(a){this.Jb=a-24}}var ad=0,bd=0;function eb(a,b,c){a>>>=0;var d=new $c(a);b>>>=0;c>>>=0;I()[d.Jb+16>>>2>>>0]=0;I()[d.Jb+4>>>2>>>0]=b;I()[d.Jb+8>>>2>>>0]=c;ad=a;bd++;throw ad;}
function cd(a,b,c,d){return q?Q(2,1,a,b,c,d):fb(a,b,c,d)}function fb(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;if("undefined"==typeof SharedArrayBuffer)return 6;var f=[];if(q&&0===f.length)return cd(a,b,c,d);a={fc:c,Bb:a,Ib:d,Nb:f};return q?(a.Cb="spawnThread",postMessage(a,f),0):Mc(a)}
var dd="undefined"!=typeof TextDecoder?new TextDecoder:void 0,ed=(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&&dd)return dd.decode(a.buffer instanceof ArrayBuffer?a.subarray(b,c):a.slice(b,c));for(d="";b<c;){var f=a[b++];if(f&128){var g=a[b++]&63;if(192==(f&224))d+=String.fromCharCode((f&31)<<6|g);else{var h=a[b++]&63;f=224==(f&240)?(f&15)<<12|g<<6|h:(f&7)<<18|g<<12|h<<6|a[b++]&63;65536>f?d+=String.fromCharCode(f):(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|
f&1023))}}else d+=String.fromCharCode(f)}return d},M=(a,b)=>(a>>>=0)?ed(F(),a,b):"";function gb(a,b,c){return q?Q(3,1,a,b,c):0}function hb(a,b){if(q)return Q(4,1,a,b)}
var fd=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},gd=(a,b,c)=>{var d=F();b>>>=0;if(0<c){var f=b;c=b+c-1;for(var g=0;g<a.length;++g){var h=a.charCodeAt(g);if(55296<=h&&57343>=h){var l=a.charCodeAt(++g);h=65536+((h&1023)<<10)|l&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-f}else a=0;return a};function ib(a,b){if(q)return Q(5,1,a,b)}function jb(a,b,c){if(q)return Q(6,1,a,b,c)}function kb(a,b,c){return q?Q(7,1,a,b,c):0}function lb(a,b){if(q)return Q(8,1,a,b)}function mb(a,b,c){if(q)return Q(9,1,a,b,c)}function nb(a,b,c,d){if(q)return Q(10,1,a,b,c,d)}function ob(a,b,c,d){if(q)return Q(11,1,a,b,c,d)}function pb(a,b,c,d){if(q)return Q(12,1,a,b,c,d)}function qb(a){if(q)return Q(13,1,a)}
function rb(a,b){if(q)return Q(14,1,a,b)}function sb(a,b,c){if(q)return Q(15,1,a,b,c)}var tb=()=>L(""),hd,R=a=>{for(var b="";F()[a>>>0];)b+=hd[F()[a++>>>0]];return b},jd={},kd={},ld={},S;function md(a,b,c={}){var d=b.name;if(!a)throw new S(`type "${d}" must have a positive integer typeid pointer`);if(kd.hasOwnProperty(a)){if(c.Tb)return;throw new S(`Cannot register type '${d}' twice`);}kd[a]=b;delete ld[a];jd.hasOwnProperty(a)&&(b=jd[a],delete jd[a],b.forEach(f=>f()))}
function T(a,b,c={}){return md(a,b,c)}var nd=(a,b,c)=>{switch(b){case 1:return c?d=>D()[d>>>0]:d=>F()[d>>>0];case 2:return c?d=>G()[d>>>1>>>0]:d=>Fa()[d>>>1>>>0];case 4:return c?d=>H()[d>>>2>>>0]:d=>I()[d>>>2>>>0];case 8:return c?d=>C[d>>>3]:d=>Da[d>>>3];default:throw new TypeError(`invalid integer width (${b}): ${a}`);}};
function ub(a,b,c){a>>>=0;c>>>=0;b=R(b>>>0);T(a,{name:b,fromWireType:d=>d,toWireType:function(d,f){if("bigint"!=typeof f&&"number"!=typeof f)throw null===f?f="null":(d=typeof f,f="object"===d||"array"===d||"function"===d?f.toString():""+f),new TypeError(`Cannot convert "${f}" to ${this.name}`);"number"==typeof f&&(f=BigInt(f));return f},Db:U,readValueFromPointer:nd(b,c,-1==b.indexOf("u")),Eb:null})}var U=8;
function vb(a,b,c,d){a>>>=0;b=R(b>>>0);T(a,{name:b,fromWireType:function(f){return!!f},toWireType:function(f,g){return g?c:d},Db:U,readValueFromPointer:function(f){return this.fromWireType(F()[f>>>0])},Eb:null})}var od=[],V=[];function Ob(a){a>>>=0;9<a&&0===--V[a+1]&&(V[a]=void 0,od.push(a))}
var W=a=>{if(!a)throw new S("Cannot use deleted val. handle = "+a);return V[a]},X=a=>{switch(a){case void 0:return 2;case null:return 4;case !0:return 6;case !1:return 8;default:const b=od.pop()||V.length;V[b]=a;V[b+1]=1;return b}};function pd(a){return this.fromWireType(I()[a>>>2>>>0])}var qd={name:"emscripten::val",fromWireType:a=>{var b=W(a);Ob(a);return b},toWireType:(a,b)=>X(b),Db:U,readValueFromPointer:pd,Eb:null};function wb(a){return T(a>>>0,qd)}
var rd=(a,b)=>{switch(b){case 4:return function(c){return this.fromWireType(Ga()[c>>>2>>>0])};case 8:return function(c){return this.fromWireType(J()[c>>>3>>>0])};default:throw new TypeError(`invalid float width (${b}): ${a}`);}};function xb(a,b,c){a>>>=0;c>>>=0;b=R(b>>>0);T(a,{name:b,fromWireType:d=>d,toWireType:(d,f)=>f,Db:U,readValueFromPointer:rd(b,c),Eb:null})}
function yb(a,b,c,d,f){a>>>=0;c>>>=0;b=R(b>>>0);-1===f&&(f=4294967295);f=l=>l;if(0===d){var g=32-8*c;f=l=>l<<g>>>g}var h=b.includes("unsigned")?function(l,m){return m>>>0}:function(l,m){return m};T(a,{name:b,fromWireType:f,toWireType:h,Db:U,readValueFromPointer:nd(b,c,0!==d),Eb:null})}
function zb(a,b,c){function d(g){var h=I()[g>>>2>>>0];g=I()[g+4>>>2>>>0];return new f(D().buffer,g,h)}a>>>=0;var f=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array][b];c=R(c>>>0);T(a,{name:c,fromWireType:d,Db:U,readValueFromPointer:d},{Tb:!0})}
function Ab(a,b){a>>>=0;b=R(b>>>0);T(a,{name:b,fromWireType:function(c){for(var d=I()[c>>>2>>>0],f=c+4,g,h=f,l=0;l<=d;++l){var m=f+l;if(l==d||0==F()[m>>>0])h=M(h,m-h),void 0===g?g=h:(g+=String.fromCharCode(0),g+=h),h=m+1}Y(c);return g},toWireType:function(c,d){d instanceof ArrayBuffer&&(d=new Uint8Array(d));var f="string"==typeof d;if(!(f||d instanceof Uint8Array||d instanceof Uint8ClampedArray||d instanceof Int8Array))throw new S("Cannot pass non-string to std::string");var g=f?fd(d):d.length;var h=
sd(4+g+1),l=h+4;I()[h>>>2>>>0]=g;if(f)gd(d,l,g+1);else if(f)for(f=0;f<g;++f){var m=d.charCodeAt(f);if(255<m)throw Y(h),new S("String has UTF-16 code units that do not fit in 8 bits");F()[l+f>>>0]=m}else for(f=0;f<g;++f)F()[l+f>>>0]=d[f];null!==c&&c.push(Y,h);return h},Db:U,readValueFromPointer:pd,Eb(c){Y(c)}})}
var td="undefined"!=typeof TextDecoder?new TextDecoder("utf-16le"):void 0,ud=(a,b)=>{var c=a>>1;for(var d=c+b/2;!(c>=d)&&Fa()[c>>>0];)++c;c<<=1;if(32<c-a&&td)return td.decode(F().slice(a,c));c="";for(d=0;!(d>=b/2);++d){var f=G()[a+2*d>>>1>>>0];if(0==f)break;c+=String.fromCharCode(f)}return c},vd=(a,b,c)=>{c??=2147483647;if(2>c)return 0;c-=2;var d=b;c=c<2*a.length?c/2:a.length;for(var f=0;f<c;++f){var g=a.charCodeAt(f);G()[b>>>1>>>0]=g;b+=2}G()[b>>>1>>>0]=0;return b-d},wd=a=>2*a.length,xd=(a,b)=>{for(var c=
0,d="";!(c>=b/4);){var f=H()[a+4*c>>>2>>>0];if(0==f)break;++c;65536<=f?(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|f&1023)):d+=String.fromCharCode(f)}return d},yd=(a,b,c)=>{b>>>=0;c??=2147483647;if(4>c)return 0;var d=b;c=d+c-4;for(var f=0;f<a.length;++f){var g=a.charCodeAt(f);if(55296<=g&&57343>=g){var h=a.charCodeAt(++f);g=65536+((g&1023)<<10)|h&1023}H()[b>>>2>>>0]=g;b+=4;if(b+4>c)break}H()[b>>>2>>>0]=0;return b-d},zd=a=>{for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);55296<=d&&57343>=
d&&++c;b+=4}return b};
function Bb(a,b,c){a>>>=0;b>>>=0;c>>>=0;c=R(c);if(2===b){var d=ud;var f=vd;var g=wd;var h=l=>Fa()[l>>>1>>>0]}else 4===b&&(d=xd,f=yd,g=zd,h=l=>I()[l>>>2>>>0]);T(a,{name:c,fromWireType:l=>{for(var m=I()[l>>>2>>>0],p,r=l+4,u=0;u<=m;++u){var w=l+4+u*b;if(u==m||0==h(w))r=d(r,w-r),void 0===p?p=r:(p+=String.fromCharCode(0),p+=r),r=w+b}Y(l);return p},toWireType:(l,m)=>{if("string"!=typeof m)throw new S(`Cannot pass non-string to C++ string type ${c}`);var p=g(m),r=sd(4+p+b);I()[r>>>2>>>0]=p/b;f(m,r+4,p+b);
null!==l&&l.push(Y,r);return r},Db:U,readValueFromPointer:pd,Eb(l){Y(l)}})}function Cb(a,b){a>>>=0;b=R(b>>>0);T(a,{Ub:!0,name:b,Db:0,fromWireType:()=>{},toWireType:()=>{}})}function Db(a){Ma(a>>>0,!k,1,!ea,131072,!1);Na()}var Ad=a=>{if(!A)try{if(a(),!(0<P))try{q?Zc(wa):yc(wa)}catch(b){b instanceof Gc||"unwind"==b||ma(1,b)}}catch(b){b instanceof Gc||"unwind"==b||ma(1,b)}};
function Oa(a){a>>>=0;"function"===typeof Atomics.kc&&(Atomics.kc(H(),a>>>2,a).value.then(Ra),a+=128,Atomics.store(H(),a>>>2,1))}var Ra=()=>{var a=Ka();a&&(Oa(a),Ad(Bd))};function Eb(a,b){a>>>=0;a==b>>>0?setTimeout(Ra):q?postMessage({Hb:a,Cb:"checkMailbox"}):(a=O[a])&&a.postMessage({Cb:"checkMailbox"})}var Cd=[];function Fb(a,b,c,d,f){b>>>=0;d/=2;Cd.length=d;c=f>>>0>>>3;for(f=0;f<d;f++)Cd[f]=C[c+2*f]?C[c+2*f+1]:J()[c+2*f+1>>>0];return(b?Ec[b]:Dd[a])(...Cd)}var Gb=()=>{P=0};
function Hb(a){a>>>=0;q?postMessage({Cb:"cleanupThread",ic:a}):Wc(O[a])}function Ib(a){n&&O[a>>>0].ref()}var Fd=(a,b)=>{var c=kd[a];if(void 0===c)throw a=Ed(a),c=R(a),Y(a),new S(`${b} has unknown type ${c}`);return c},Gd=(a,b,c)=>{var d=[];a=a.toWireType(d,c);d.length&&(I()[b>>>2>>>0]=X(d));return a};function Jb(a,b,c){b>>>=0;c>>>=0;a=W(a>>>0);b=Fd(b,"emval::as");return Gd(b,c,a)}function Kb(a,b){b>>>=0;a=W(a>>>0);b=Fd(b,"emval::as");return b.toWireType(null,a)}var Hd=a=>{try{a()}catch(b){L(b)}};
function Id(){var a=K,b={};for(let [c,d]of Object.entries(a))b[c]="function"==typeof d?(...f)=>{Jd.push(c);try{return d(...f)}finally{A||(Jd.pop(),t&&1===Z&&0===Jd.length&&(Z=0,P+=1,Hd(Kd),"undefined"!=typeof Fibers&&Fibers.sc()))}}:d;return b}var Z=0,t=null,Ld=0,Jd=[],Md={},Nd={},Od=0,Pd=null,Qd=[];function ia(){return new Promise((a,b)=>{Pd={resolve:a,reject:b}})}
function Rd(){var a=sd(65548),b=a+12;I()[a>>>2>>>0]=b;I()[a+4>>>2>>>0]=b+65536;b=Jd[0];var c=Md[b];void 0===c&&(c=Od++,Md[b]=c,Nd[c]=b);b=c;H()[a+8>>>2>>>0]=b;return a}function Sd(){var a=H()[t+8>>>2>>>0];a=K[Nd[a]];--P;return a()}
function Td(a){if(!A){if(0===Z){var b=!1,c=!1;a((d=0)=>{if(!A&&(Ld=d,b=!0,c)){Z=2;Hd(()=>Ud(t));"undefined"!=typeof MainLoop&&MainLoop.Qb&&MainLoop.resume();d=!1;try{var f=Sd()}catch(l){f=l,d=!0}var g=!1;if(!t){var h=Pd;h&&(Pd=null,(d?h.reject:h.resolve)(f),g=!0)}if(d&&!g)throw f;}});c=!0;b||(Z=1,t=Rd(),"undefined"!=typeof MainLoop&&MainLoop.Qb&&MainLoop.pause(),Hd(()=>Vd(t)))}else 2===Z?(Z=0,Hd(Xd),Y(t),t=null,Qd.forEach(Ad)):L(`invalid state: ${Z}`);return Ld}}
function Fc(a){return Td(b=>{a().then(b)})}function Lb(a){a>>>=0;return Fc(async()=>{var b=await W(a);return X(b)})}var Yd=[];function Mb(a,b,c,d){c>>>=0;d>>>=0;a=Yd[a>>>0];b=W(b>>>0);return a(null,b,c,d)}var Zd={},$d=a=>{var b=Zd[a];return void 0===b?R(a):b};function Nb(a,b,c,d,f){c>>>=0;d>>>=0;f>>>=0;a=Yd[a>>>0];b=W(b>>>0);c=$d(c);return a(b,b[c],d,f)}function Pb(a,b){b>>>=0;a=W(a>>>0);b=W(b);return a==b}var ae=()=>"object"==typeof globalThis?globalThis:Function("return this")();
function Qb(a){a>>>=0;if(0===a)return X(ae());a=$d(a);return X(ae()[a])}var be=a=>{var b=Yd.length;Yd.push(a);return b},ce=(a,b)=>{for(var c=Array(a),d=0;d<a;++d)c[d]=Fd(I()[b+4*d>>>2>>>0],"parameter "+d);return c},de=(a,b)=>Object.defineProperty(b,"name",{value:a});
function ee(a){var b=Function;if(!(b instanceof Function))throw new TypeError(`new_ called with constructor type ${typeof b} which is not a function`);var c=de(b.name||"unknownFunctionName",function(){});c.prototype=b.prototype;c=new c;a=b.apply(c,a);return a instanceof Object?a:c}
function Rb(a,b,c){b=ce(a,b>>>0);var d=b.shift();a--;var f="return function (obj, func, destructorsRef, args) {\n",g=0,h=[];0===c&&h.push("obj");for(var l=["retType"],m=[d],p=0;p<a;++p)h.push("arg"+p),l.push("argType"+p),m.push(b[p]),f+=` var arg${p} = argType${p}.readValueFromPointer(args${g?"+"+g:""});\n`,g+=b[p].Db;f+=` var rv = ${1===c?"new func":"func.call"}(${h.join(", ")});\n`;d.Ub||(l.push("emval_returnValue"),m.push(Gd),f+=" return emval_returnValue(retType, destructorsRef, rv);\n");l.push(f+
"};\n");a=ee(l)(...m);c=`methodCaller<(${b.map(r=>r.name).join(", ")}) => ${d.name}>`;return be(de(c,a))}function Sb(a){a=$d(a>>>0);return X(e[a])}function Tb(a,b){b>>>=0;a=W(a>>>0);b=W(b);return X(a[b])}function Ub(a){a>>>=0;9<a&&(V[a+1]+=1)}function Vb(){return X([])}function Wb(a){a=W(a>>>0);for(var b=Array(a.length),c=0;c<a.length;c++)b[c]=a[c];return X(b)}function Xb(a){return X($d(a>>>0))}function Yb(){return X({})}
function Zb(a){a>>>=0;for(var b=W(a);b.length;){var c=b.pop();b.pop()(c)}Ob(a)}function $b(a,b,c){b>>>=0;c>>>=0;a=W(a>>>0);b=W(b);c=W(c);a[b]=c}function ac(a,b){b>>>=0;a=Fd(a>>>0,"_emval_take_value");a=a.readValueFromPointer(b);return X(a)}
function bc(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);H()[b>>>2>>>0]=a.getUTCSeconds();H()[b+4>>>2>>>0]=a.getUTCMinutes();H()[b+8>>>2>>>0]=a.getUTCHours();H()[b+12>>>2>>>0]=a.getUTCDate();H()[b+16>>>2>>>0]=a.getUTCMonth();H()[b+20>>>2>>>0]=a.getUTCFullYear()-1900;H()[b+24>>>2>>>0]=a.getUTCDay();a=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0;H()[b+28>>>2>>>0]=a}
var fe=a=>0===a%4&&(0!==a%100||0===a%400),ge=[0,31,60,91,121,152,182,213,244,274,305,335],he=[0,31,59,90,120,151,181,212,243,273,304,334];
function cc(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);H()[b>>>2>>>0]=a.getSeconds();H()[b+4>>>2>>>0]=a.getMinutes();H()[b+8>>>2>>>0]=a.getHours();H()[b+12>>>2>>>0]=a.getDate();H()[b+16>>>2>>>0]=a.getMonth();H()[b+20>>>2>>>0]=a.getFullYear()-1900;H()[b+24>>>2>>>0]=a.getDay();var c=(fe(a.getFullYear())?ge:he)[a.getMonth()]+a.getDate()-1|0;H()[b+28>>>2>>>0]=c;H()[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;H()[b+32>>>2>>>0]=a}
function dc(a){a>>>=0;var b=new Date(H()[a+20>>>2>>>0]+1900,H()[a+16>>>2>>>0],H()[a+12>>>2>>>0],H()[a+8>>>2>>>0],H()[a+4>>>2>>>0],H()[a>>>2>>>0],0),c=H()[a+32>>>2>>>0],d=b.getTimezoneOffset(),f=(new Date(b.getFullYear(),6,1)).getTimezoneOffset(),g=(new Date(b.getFullYear(),0,1)).getTimezoneOffset(),h=Math.min(g,f);0>c?H()[a+32>>>2>>>0]=Number(f!=g&&h==d):0<c!=(h==d)&&(f=Math.max(g,f),b.setTime(b.getTime()+6E4*((0<c?h:f)-d)));H()[a+24>>>2>>>0]=b.getDay();c=(fe(b.getFullYear())?ge:he)[b.getMonth()]+
b.getDate()-1|0;H()[a+28>>>2>>>0]=c;H()[a>>>2>>>0]=b.getSeconds();H()[a+4>>>2>>>0]=b.getMinutes();H()[a+8>>>2>>>0]=b.getHours();H()[a+12>>>2>>>0]=b.getDate();H()[a+16>>>2>>>0]=b.getMonth();H()[a+20>>>2>>>0]=b.getYear();a=b.getTime();return BigInt(isNaN(a)?-1:a/1E3)}function ec(a,b,c,d,f,g,h){return q?Q(16,1,a,b,c,d,f,g,h):-52}function fc(a,b,c,d,f,g){if(q)return Q(17,1,a,b,c,d,f,g)}var ie={},qc=()=>performance.timeOrigin+performance.now();
function gc(a,b){if(q)return Q(18,1,a,b);ie[a]&&(clearTimeout(ie[a].id),delete ie[a]);if(!b)return 0;var c=setTimeout(()=>{delete ie[a];Ad(()=>je(a,performance.timeOrigin+performance.now()))},b);ie[a]={id:c,rc:b};return 0}
function hc(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;var f=(new Date).getFullYear(),g=(new Date(f,0,1)).getTimezoneOffset();f=(new Date(f,6,1)).getTimezoneOffset();var h=Math.max(g,f);I()[a>>>2>>>0]=60*h;H()[b>>>2>>>0]=Number(g!=f);b=l=>{var m=Math.abs(l);return`UTC${0<=l?"-":"+"}${String(Math.floor(m/60)).padStart(2,"0")}${String(m%60).padStart(2,"0")}`};a=b(g);b=b(f);f<g?(gd(a,c,17),gd(b,d,17)):(gd(a,d,17),gd(b,c,17))}var mc=()=>Date.now(),ke=1;
function ic(a,b,c){if(!(0<=a&&3>=a))return 28;if(0===a)a=Date.now();else if(ke)a=performance.timeOrigin+performance.now();else return 52;C[c>>>0>>>3]=BigInt(Math.round(1E6*a));return 0}var le=[],me=(a,b)=>{le.length=0;for(var c;c=F()[a++>>>0];){var d=105!=c;d&=112!=c;b+=d&&b%8?4:0;le.push(112==c?I()[b>>>2>>>0]:106==c?C[b>>>3]:105==c?H()[b>>>2>>>0]:J()[b>>>3>>>0]);b+=d?8:4}return le};function jc(a,b,c){a>>>=0;b=me(b>>>0,c>>>0);return Ec[a](...b)}
function kc(a,b,c){a>>>=0;b=me(b>>>0,c>>>0);return Ec[a](...b)}var lc=()=>{};function nc(a,b){return x(M(a>>>0,b>>>0))}var oc=()=>{P+=1;throw"unwind";};function pc(){return 4294901760}var rc=()=>n?require("os").cpus().length:navigator.hardwareConcurrency;function sc(){L("Cannot use emscripten_pc_get_function without -sUSE_OFFSET_CONVERTER");return 0}
function tc(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))-z.buffer.byteLength+65535)/65536|0;try{z.grow(d);E();var f=1;break a}catch(g){}f=void 0}if(f)return!0}return!1}var ne=()=>{L("Cannot use convertFrameToPC (needed by __builtin_return_address) without -sUSE_OFFSET_CONVERTER");return 0},oe={},pe=a=>{a.forEach(b=>{var c=ne();c&&(oe[c]=b)})};
function uc(){var a=Error().stack.toString().split("\n");"Error"==a[0]&&a.shift();pe(a);oe.Mb=ne();oe.dc=a;return oe.Mb}function vc(a,b,c){a>>>=0;b>>>=0;if(oe.Mb==a)var d=oe.dc;else d=Error().stack.toString().split("\n"),"Error"==d[0]&&d.shift(),pe(d);for(var f=3;d[f]&&ne()!=a;)++f;for(a=0;a<c&&d[a+f];++a)H()[b+4*a>>>2>>>0]=ne();return a}
var qe={},se=()=>{if(!re){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",_:la||"./this.program"},b;for(b in qe)void 0===qe[b]?delete a[b]:a[b]=qe[b];var c=[];for(b in a)c.push(`${b}=${a[b]}`);re=c}return re},re;
function wc(a,b){if(q)return Q(19,1,a,b);a>>>=0;b>>>=0;var c=0;se().forEach((d,f)=>{var g=b+c;f=I()[a+4*f>>>2>>>0]=g;for(g=0;g<d.length;++g)D()[f++>>>0]=d.charCodeAt(g);D()[f>>>0]=0;c+=d.length+1});return 0}function xc(a,b){if(q)return Q(20,1,a,b);a>>>=0;b>>>=0;var c=se();I()[a>>>2>>>0]=c.length;var d=0;c.forEach(f=>d+=f.length+1);I()[b>>>2>>>0]=d;return 0}function zc(a){return q?Q(21,1,a):52}function Ac(a,b,c,d){return q?Q(22,1,a,b,c,d):52}function Bc(a,b,c,d){return q?Q(23,1,a,b,c,d):70}
var te=[null,[],[]];function Cc(a,b,c,d){if(q)return Q(24,1,a,b,c,d);b>>>=0;c>>>=0;d>>>=0;for(var f=0,g=0;g<c;g++){var h=I()[b>>>2>>>0],l=I()[b+4>>>2>>>0];b+=8;for(var m=0;m<l;m++){var p=F()[h+m>>>0],r=te[a];0===p||10===p?((1===a?ta:x)(ed(r)),r.length=0):r.push(p)}f+=l}I()[d>>>2>>>0]=f;return 0}q||Tc();for(var ue=Array(256),ve=0;256>ve;++ve)ue[ve]=String.fromCharCode(ve);hd=ue;S=e.BindingError=class extends Error{constructor(a){super(a);this.name="BindingError"}};
e.InternalError=class extends Error{constructor(a){super(a);this.name="InternalError"}};V.push(0,1,void 0,1,null,1,!0,1,!1,1);e.count_emval_handles=()=>V.length/2-5-od.length;var Dd=[Dc,Rc,cd,gb,hb,ib,jb,kb,lb,mb,nb,ob,pb,qb,rb,sb,ec,fc,gc,wc,xc,zc,Ac,Bc,Cc],bb,K;
(async function(){function a(d,f){K=d.exports;K=Id();K=we();Sc.push(K.jb);va=f;Wa();return K}Ua++;var b=ab();if(e.instantiateWasm)return new Promise(d=>{e.instantiateWasm(b,(f,g)=>{a(f,g);d(f.exports)})});if(q)return new Promise(d=>{Ha=f=>{var g=new WebAssembly.Instance(f,ab());d(a(g,f))}});Xa??=e.locateFile?e.locateFile?e.locateFile("ort-wasm-simd-threaded.jsep.wasm",v):v+"ort-wasm-simd-threaded.jsep.wasm":(new URL("ort-wasm-simd-threaded.jsep.wasm",import.meta.url)).href;try{var c=await $a(b);
return a(c.instance,c.module)}catch(d){return ca(d),Promise.reject(d)}})();var Ed=a=>(Ed=K.Ea)(a),Pa=()=>(Pa=K.Fa)();e._OrtInit=(a,b)=>(e._OrtInit=K.Ga)(a,b);e._OrtGetLastError=(a,b)=>(e._OrtGetLastError=K.Ha)(a,b);e._OrtCreateSessionOptions=(a,b,c,d,f,g,h,l,m,p)=>(e._OrtCreateSessionOptions=K.Ia)(a,b,c,d,f,g,h,l,m,p);e._OrtAppendExecutionProvider=(a,b,c,d,f)=>(e._OrtAppendExecutionProvider=K.Ja)(a,b,c,d,f);e._OrtAddFreeDimensionOverride=(a,b,c)=>(e._OrtAddFreeDimensionOverride=K.Ka)(a,b,c);
e._OrtAddSessionConfigEntry=(a,b,c)=>(e._OrtAddSessionConfigEntry=K.La)(a,b,c);e._OrtReleaseSessionOptions=a=>(e._OrtReleaseSessionOptions=K.Ma)(a);e._OrtCreateSession=(a,b,c)=>(e._OrtCreateSession=K.Na)(a,b,c);e._OrtReleaseSession=a=>(e._OrtReleaseSession=K.Oa)(a);e._OrtGetInputOutputCount=(a,b,c)=>(e._OrtGetInputOutputCount=K.Pa)(a,b,c);e._OrtGetInputOutputMetadata=(a,b,c,d)=>(e._OrtGetInputOutputMetadata=K.Qa)(a,b,c,d);e._OrtFree=a=>(e._OrtFree=K.Ra)(a);
e._OrtCreateTensor=(a,b,c,d,f,g)=>(e._OrtCreateTensor=K.Sa)(a,b,c,d,f,g);e._OrtGetTensorData=(a,b,c,d,f)=>(e._OrtGetTensorData=K.Ta)(a,b,c,d,f);e._OrtReleaseTensor=a=>(e._OrtReleaseTensor=K.Ua)(a);e._OrtCreateRunOptions=(a,b,c,d)=>(e._OrtCreateRunOptions=K.Va)(a,b,c,d);e._OrtAddRunConfigEntry=(a,b,c)=>(e._OrtAddRunConfigEntry=K.Wa)(a,b,c);e._OrtReleaseRunOptions=a=>(e._OrtReleaseRunOptions=K.Xa)(a);e._OrtCreateBinding=a=>(e._OrtCreateBinding=K.Ya)(a);
e._OrtBindInput=(a,b,c)=>(e._OrtBindInput=K.Za)(a,b,c);e._OrtBindOutput=(a,b,c,d)=>(e._OrtBindOutput=K._a)(a,b,c,d);e._OrtClearBoundOutputs=a=>(e._OrtClearBoundOutputs=K.$a)(a);e._OrtReleaseBinding=a=>(e._OrtReleaseBinding=K.ab)(a);e._OrtRunWithBinding=(a,b,c,d,f)=>(e._OrtRunWithBinding=K.bb)(a,b,c,d,f);e._OrtRun=(a,b,c,d,f,g,h,l)=>(e._OrtRun=K.cb)(a,b,c,d,f,g,h,l);e._OrtEndProfiling=a=>(e._OrtEndProfiling=K.db)(a);e._JsepOutput=(a,b,c)=>(e._JsepOutput=K.eb)(a,b,c);
e._JsepGetNodeName=a=>(e._JsepGetNodeName=K.fb)(a);
var Ka=()=>(Ka=K.gb)(),Y=e._free=a=>(Y=e._free=K.hb)(a),sd=e._malloc=a=>(sd=e._malloc=K.ib)(a),Ma=(a,b,c,d,f,g)=>(Ma=K.lb)(a,b,c,d,f,g),Sa=()=>(Sa=K.mb)(),Pc=(a,b,c,d,f)=>(Pc=K.nb)(a,b,c,d,f),Vc=a=>(Vc=K.ob)(a),Zc=a=>(Zc=K.pb)(a),je=(a,b)=>(je=K.qb)(a,b),Bd=()=>(Bd=K.rb)(),Xc=(a,b)=>(Xc=K.sb)(a,b),Qc=a=>(Qc=K.tb)(a),Oc=a=>(Oc=K.ub)(a),Nc=()=>(Nc=K.vb)(),Yc=e.dynCall_ii=(a,b)=>(Yc=e.dynCall_ii=K.wb)(a,b),Vd=a=>(Vd=K.xb)(a),Kd=()=>(Kd=K.yb)(),Ud=a=>(Ud=K.zb)(a),Xd=()=>(Xd=K.Ab)();
function we(){var a=K;a=Object.assign({},a);var b=d=>f=>d(f)>>>0,c=d=>()=>d()>>>0;a.Ea=b(a.Ea);a.gb=c(a.gb);a.ib=b(a.ib);a.ub=b(a.ub);a.vb=c(a.vb);a.__cxa_get_exception_ptr=b(a.__cxa_get_exception_ptr);return a}e.stackSave=()=>Nc();e.stackRestore=a=>Qc(a);e.stackAlloc=a=>Oc(a);
e.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":G()[a>>>1>>>0]=b;break;case "i32":H()[a>>>2>>>0]=b;break;case "i64":C[a>>>3]=BigInt(b);break;case "float":Ga()[a>>>2>>>0]=b;break;case "double":J()[a>>>3>>>0]=b;break;case "*":I()[a>>>2>>>0]=b;break;default:L(`invalid type for setValue: ${c}`)}};
e.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 G()[a>>>1>>>0];case "i32":return H()[a>>>2>>>0];case "i64":return C[a>>>3];case "float":return Ga()[a>>>2>>>0];case "double":return J()[a>>>3>>>0];case "*":return I()[a>>>2>>>0];default:L(`invalid type for getValue: ${b}`)}};e.UTF8ToString=M;e.stringToUTF8=gd;e.lengthBytesUTF8=fd;
function xe(){if(0<Ua)Va=xe;else if(q)aa(e),Ta();else{for(;0<Ic.length;)Ic.shift()(e);0<Ua?Va=xe:(e.calledRun=!0,A||(Ta(),aa(e)))}}xe();e.PTR_SIZE=4;moduleRtn=da;
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();

View File

@@ -0,0 +1,70 @@
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();

View File

@@ -0,0 +1,324 @@
let wasm;
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
export function main() {
wasm.main();
}
let cachedFloat32ArrayMemory0 = null;
function getFloat32ArrayMemory0() {
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
}
return cachedFloat32ArrayMemory0;
}
function passArrayF32ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 4, 4) >>> 0;
getFloat32ArrayMemory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function getArrayF32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
const SIMDMathFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_simdmath_free(ptr >>> 0, 1));
export class SIMDMath {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
SIMDMathFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_simdmath_free(ptr, 0);
}
constructor() {
const ret = wasm.simdmath_new();
this.__wbg_ptr = ret >>> 0;
SIMDMathFinalization.register(this, this.__wbg_ptr, this);
return this;
}
/**
* @param {Float32Array} vec_a
* @param {Float32Array} vec_b
* @returns {number}
*/
cosine_similarity(vec_a, vec_b) {
const ptr0 = passArrayF32ToWasm0(vec_a, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(vec_b, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.simdmath_cosine_similarity(this.__wbg_ptr, ptr0, len0, ptr1, len1);
return ret;
}
/**
* @param {Float32Array} vectors
* @param {Float32Array} query
* @param {number} vector_dim
* @returns {Float32Array}
*/
batch_similarity(vectors, query, vector_dim) {
const ptr0 = passArrayF32ToWasm0(vectors, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(query, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.simdmath_batch_similarity(this.__wbg_ptr, ptr0, len0, ptr1, len1, vector_dim);
var v3 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v3;
}
/**
* @param {Float32Array} vectors_a
* @param {Float32Array} vectors_b
* @param {number} vector_dim
* @returns {Float32Array}
*/
similarity_matrix(vectors_a, vectors_b, vector_dim) {
const ptr0 = passArrayF32ToWasm0(vectors_a, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(vectors_b, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.simdmath_similarity_matrix(this.__wbg_ptr, ptr0, len0, ptr1, len1, vector_dim);
var v3 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v3;
}
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
console.error(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
};
imports.wbg.__wbg_new_8a6f238a6ece86ea = function() {
const ret = new Error();
return ret;
};
imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) {
const ret = arg1.stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_3;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
;
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedDataViewMemory0 = null;
cachedFloat32ArrayMemory0 = null;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined') {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('simd_math_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;

Binary file not shown.

View File

@@ -0,0 +1,384 @@
/* eslint-disable */
// js/similarity.worker.js
importScripts('../libs/ort.min.js'); // 调整路径以匹配您的文件结构
// 全局Worker状态
let session = null;
let modelPathInternal = null;
let ortEnvConfigured = false;
let sessionOptions = null;
let modelInputNames = null; // 存储模型的输入名称
// 复用的 TypedArray 缓冲区,减少内存分配
let reusableBuffers = {
inputIds: null,
attentionMask: null,
tokenTypeIds: null,
};
// 性能统计
let workerStats = {
totalInferences: 0,
totalInferenceTime: 0,
averageInferenceTime: 0,
memoryAllocations: 0,
};
// 配置 ONNX Runtime 环境 (仅一次)
function configureOrtEnv(numThreads = 1, executionProviders = ['wasm']) {
if (ortEnvConfigured) return;
try {
ort.env.wasm.numThreads = numThreads;
ort.env.wasm.simd = true; // 尽可能启用SIMD
ort.env.wasm.proxy = false; // 在Worker中通常不需要代理
ort.env.logLevel = 'warning'; // 'verbose', 'info', 'warning', 'error', 'fatal'
ortEnvConfigured = true;
sessionOptions = {
executionProviders: executionProviders,
graphOptimizationLevel: 'all',
enableCpuMemArena: true,
enableMemPattern: true,
// executionMode: 'sequential' // 在worker内部通常是顺序执行一个任务
};
} catch (error) {
console.error('Worker: Failed to configure ORT environment', error);
throw error; // 抛出错误,让主线程知道
}
}
async function initializeModel(modelPathOrData, numThreads, executionProviders) {
try {
configureOrtEnv(numThreads, executionProviders); // 确保环境已配置
if (!modelPathOrData) {
throw new Error('Worker: Model path or data is not provided.');
}
// Check if input is ArrayBuffer (cached model data) or string (URL path)
if (modelPathOrData instanceof ArrayBuffer) {
console.log(
`Worker: Initializing model from cached ArrayBuffer (${modelPathOrData.byteLength} bytes)`,
);
session = await ort.InferenceSession.create(modelPathOrData, sessionOptions);
modelPathInternal = '[Cached ArrayBuffer]'; // For debugging purposes
} else {
console.log(`Worker: Initializing model from URL: ${modelPathOrData}`);
modelPathInternal = modelPathOrData; // 存储模型路径以备调试或重载(如果需要)
session = await ort.InferenceSession.create(modelPathInternal, sessionOptions);
}
// 获取模型的输入名称用于判断是否需要token_type_ids
modelInputNames = session.inputNames;
console.log(`Worker: ONNX session created successfully for model: ${modelPathInternal}`);
console.log(`Worker: Model input names:`, modelInputNames);
return { status: 'success', message: 'Model initialized' };
} catch (error) {
console.error(`Worker: Model initialization failed:`, error);
session = null; // 清理session以防部分初始化
modelInputNames = null;
// 将错误信息序列化因为Error对象本身可能无法直接postMessage
throw new Error(`Worker: Model initialization failed - ${error.message}`);
}
}
// 优化的缓冲区管理函数
function getOrCreateBuffer(name, requiredLength, type = BigInt64Array) {
if (!reusableBuffers[name] || reusableBuffers[name].length < requiredLength) {
reusableBuffers[name] = new type(requiredLength);
workerStats.memoryAllocations++;
}
return reusableBuffers[name];
}
// 优化的批处理推理函数
async function runBatchInference(batchData) {
if (!session) {
throw new Error("Worker: Session not initialized. Call 'initializeModel' first.");
}
const startTime = performance.now();
try {
const feeds = {};
const batchSize = batchData.dims.input_ids[0];
const seqLength = batchData.dims.input_ids[1];
// 优化:复用缓冲区,减少内存分配
const inputIdsLength = batchData.input_ids.length;
const attentionMaskLength = batchData.attention_mask.length;
// 复用或创建 BigInt64Array 缓冲区
const inputIdsBuffer = getOrCreateBuffer('inputIds', inputIdsLength);
const attentionMaskBuffer = getOrCreateBuffer('attentionMask', attentionMaskLength);
// 批量填充数据(避免 map 操作)
for (let i = 0; i < inputIdsLength; i++) {
inputIdsBuffer[i] = BigInt(batchData.input_ids[i]);
}
for (let i = 0; i < attentionMaskLength; i++) {
attentionMaskBuffer[i] = BigInt(batchData.attention_mask[i]);
}
feeds['input_ids'] = new ort.Tensor(
'int64',
inputIdsBuffer.slice(0, inputIdsLength),
batchData.dims.input_ids,
);
feeds['attention_mask'] = new ort.Tensor(
'int64',
attentionMaskBuffer.slice(0, attentionMaskLength),
batchData.dims.attention_mask,
);
// 处理 token_type_ids - 只有当模型需要时才提供
if (modelInputNames && modelInputNames.includes('token_type_ids')) {
if (batchData.token_type_ids && batchData.dims.token_type_ids) {
const tokenTypeIdsLength = batchData.token_type_ids.length;
const tokenTypeIdsBuffer = getOrCreateBuffer('tokenTypeIds', tokenTypeIdsLength);
for (let i = 0; i < tokenTypeIdsLength; i++) {
tokenTypeIdsBuffer[i] = BigInt(batchData.token_type_ids[i]);
}
feeds['token_type_ids'] = new ort.Tensor(
'int64',
tokenTypeIdsBuffer.slice(0, tokenTypeIdsLength),
batchData.dims.token_type_ids,
);
} else {
// 创建默认的全零 token_type_ids
const tokenTypeIdsBuffer = getOrCreateBuffer('tokenTypeIds', inputIdsLength);
tokenTypeIdsBuffer.fill(0n, 0, inputIdsLength);
feeds['token_type_ids'] = new ort.Tensor(
'int64',
tokenTypeIdsBuffer.slice(0, inputIdsLength),
batchData.dims.input_ids,
);
}
} else {
console.log('Worker: Skipping token_type_ids as model does not require it');
}
// 执行批处理推理
const results = await session.run(feeds);
const outputTensor = results.last_hidden_state || results[Object.keys(results)[0]];
// 使用 Transferable Objects 优化数据传输
const outputData = new Float32Array(outputTensor.data);
// 更新统计信息
workerStats.totalInferences += batchSize; // 批处理计算多个推理
const inferenceTime = performance.now() - startTime;
workerStats.totalInferenceTime += inferenceTime;
workerStats.averageInferenceTime = workerStats.totalInferenceTime / workerStats.totalInferences;
return {
status: 'success',
output: {
data: outputData,
dims: outputTensor.dims,
batchSize: batchSize,
seqLength: seqLength,
},
transferList: [outputData.buffer],
stats: {
inferenceTime,
totalInferences: workerStats.totalInferences,
averageInferenceTime: workerStats.averageInferenceTime,
memoryAllocations: workerStats.memoryAllocations,
batchSize: batchSize,
},
};
} catch (error) {
console.error('Worker: Batch inference failed:', error);
throw new Error(`Worker: Batch inference failed - ${error.message}`);
}
}
async function runInference(inputData) {
if (!session) {
throw new Error("Worker: Session not initialized. Call 'initializeModel' first.");
}
const startTime = performance.now();
try {
const feeds = {};
// 优化:复用缓冲区,减少内存分配
const inputIdsLength = inputData.input_ids.length;
const attentionMaskLength = inputData.attention_mask.length;
// 复用或创建 BigInt64Array 缓冲区
const inputIdsBuffer = getOrCreateBuffer('inputIds', inputIdsLength);
const attentionMaskBuffer = getOrCreateBuffer('attentionMask', attentionMaskLength);
// 填充数据(避免 map 操作)
for (let i = 0; i < inputIdsLength; i++) {
inputIdsBuffer[i] = BigInt(inputData.input_ids[i]);
}
for (let i = 0; i < attentionMaskLength; i++) {
attentionMaskBuffer[i] = BigInt(inputData.attention_mask[i]);
}
feeds['input_ids'] = new ort.Tensor(
'int64',
inputIdsBuffer.slice(0, inputIdsLength),
inputData.dims.input_ids,
);
feeds['attention_mask'] = new ort.Tensor(
'int64',
attentionMaskBuffer.slice(0, attentionMaskLength),
inputData.dims.attention_mask,
);
// 处理 token_type_ids - 只有当模型需要时才提供
if (modelInputNames && modelInputNames.includes('token_type_ids')) {
if (inputData.token_type_ids && inputData.dims.token_type_ids) {
const tokenTypeIdsLength = inputData.token_type_ids.length;
const tokenTypeIdsBuffer = getOrCreateBuffer('tokenTypeIds', tokenTypeIdsLength);
for (let i = 0; i < tokenTypeIdsLength; i++) {
tokenTypeIdsBuffer[i] = BigInt(inputData.token_type_ids[i]);
}
feeds['token_type_ids'] = new ort.Tensor(
'int64',
tokenTypeIdsBuffer.slice(0, tokenTypeIdsLength),
inputData.dims.token_type_ids,
);
} else {
// 创建默认的全零 token_type_ids
const tokenTypeIdsBuffer = getOrCreateBuffer('tokenTypeIds', inputIdsLength);
tokenTypeIdsBuffer.fill(0n, 0, inputIdsLength);
feeds['token_type_ids'] = new ort.Tensor(
'int64',
tokenTypeIdsBuffer.slice(0, inputIdsLength),
inputData.dims.input_ids,
);
}
} else {
console.log('Worker: Skipping token_type_ids as model does not require it');
}
const results = await session.run(feeds);
const outputTensor = results.last_hidden_state || results[Object.keys(results)[0]];
// 使用 Transferable Objects 优化数据传输
const outputData = new Float32Array(outputTensor.data);
// 更新统计信息
workerStats.totalInferences++;
const inferenceTime = performance.now() - startTime;
workerStats.totalInferenceTime += inferenceTime;
workerStats.averageInferenceTime = workerStats.totalInferenceTime / workerStats.totalInferences;
return {
status: 'success',
output: {
data: outputData, // 直接返回 Float32Array
dims: outputTensor.dims,
},
transferList: [outputData.buffer], // 标记为可转移对象
stats: {
inferenceTime,
totalInferences: workerStats.totalInferences,
averageInferenceTime: workerStats.averageInferenceTime,
memoryAllocations: workerStats.memoryAllocations,
},
};
} catch (error) {
console.error('Worker: Inference failed:', error);
throw new Error(`Worker: Inference failed - ${error.message}`);
}
}
self.onmessage = async (event) => {
const { id, type, payload } = event.data;
try {
switch (type) {
case 'init':
// Support both modelPath (URL string) and modelData (ArrayBuffer)
const modelInput = payload.modelData || payload.modelPath;
await initializeModel(modelInput, payload.numThreads, payload.executionProviders);
self.postMessage({ id, type: 'init_complete', status: 'success' });
break;
case 'infer':
const result = await runInference(payload);
// 使用 Transferable Objects 优化数据传输
self.postMessage(
{
id,
type: 'infer_complete',
status: 'success',
payload: result.output,
stats: result.stats,
},
result.transferList || [],
);
break;
case 'batchInfer':
const batchResult = await runBatchInference(payload);
// 使用 Transferable Objects 优化数据传输
self.postMessage(
{
id,
type: 'batchInfer_complete',
status: 'success',
payload: batchResult.output,
stats: batchResult.stats,
},
batchResult.transferList || [],
);
break;
case 'getStats':
self.postMessage({
id,
type: 'stats_complete',
status: 'success',
payload: workerStats,
});
break;
case 'clearBuffers':
// 清理缓冲区,释放内存
reusableBuffers = {
inputIds: null,
attentionMask: null,
tokenTypeIds: null,
};
workerStats.memoryAllocations = 0;
self.postMessage({
id,
type: 'clear_complete',
status: 'success',
payload: { message: 'Buffers cleared' },
});
break;
default:
console.warn(`Worker: Unknown message type: ${type}`);
self.postMessage({
id,
type: 'error',
status: 'error',
payload: { message: `Unknown message type: ${type}` },
});
}
} catch (error) {
// 确保将错误作为普通对象发送因为Error对象本身可能无法正确序列化
self.postMessage({
id,
type: `${type}_error`, // 如 'init_error' 或 'infer_error'
status: 'error',
payload: {
message: error.message,
stack: error.stack, // 可选,用于调试
name: error.name,
},
});
}
};

View File

@@ -0,0 +1,98 @@
import { defineConfig } from 'wxt';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { config } from 'dotenv';
import { resolve } from 'path';
config({ path: resolve(process.cwd(), '.env') });
config({ path: resolve(process.cwd(), '.env.local') });
const CHROME_EXTENSION_KEY = process.env.CHROME_EXTENSION_KEY;
// See https://wxt.dev/api/config.html
export default defineConfig({
modules: ['@wxt-dev/module-vue'],
runner: {
// 方案1: 禁用自动启动(推荐)
disabled: true,
// 方案2: 如果要启用自动启动并使用现有配置,取消注释下面的配置
// chromiumArgs: [
// '--user-data-dir=' + homedir() + (process.platform === 'darwin'
// ? '/Library/Application Support/Google/Chrome'
// : process.platform === 'win32'
// ? '/AppData/Local/Google/Chrome/User Data'
// : '/.config/google-chrome'),
// '--remote-debugging-port=9222',
// ],
},
manifest: {
// Use environment variable for the key, fallback to undefined if not set
key: CHROME_EXTENSION_KEY,
default_locale: 'zh_CN',
name: '__MSG_extensionName__',
description: '__MSG_extensionDescription__',
permissions: [
'nativeMessaging',
'tabs',
'activeTab',
'scripting',
'downloads',
'webRequest',
'debugger',
'history',
'bookmarks',
'offscreen',
'storage',
],
host_permissions: ['<all_urls>'],
web_accessible_resources: [
{
resources: [
'/models/*', // 允许访问 public/models/ 下的所有文件
'/workers/*', // 允许访问 workers 文件
],
matches: ['<all_urls>'],
},
],
cross_origin_embedder_policy: {
value: 'require-corp',
},
cross_origin_opener_policy: {
value: 'same-origin',
},
content_security_policy: {
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';",
},
},
vite: (env) => ({
plugins: [
viteStaticCopy({
targets: [
{
src: 'inject-scripts/*.js',
dest: 'inject-scripts',
},
{
src: ['workers/*'],
dest: 'workers',
},
{
src: '_locales/**/*',
dest: '_locales',
},
],
}) as any,
],
build: {
// 我们的构建产物需要兼容到es6
target: 'es2015',
// 非生产环境下生成sourcemap
sourcemap: env.mode !== 'production',
// 禁用gzip 压缩大小报告,因为压缩大型文件可能会很慢
reportCompressedSize: false,
// chunk大小超过1500kb是触发警告
chunkSizeWarningLimit: 1500,
minify: false,
},
}),
});