Major refactor: Multi-user Chrome MCP extension with remote server architecture

This commit is contained in:
nasir@endelospay.com
2025-08-21 20:09:57 +05:00
parent d97cad1736
commit 5d869f6a7c
125 changed files with 16249 additions and 11906 deletions

View File

@@ -1,4 +1,10 @@
# Chrome Extension Private Key
# Copy this file to .env and replace with your actual private key
# Chrome Extension Configuration
# Copy this file to .env and replace with your actual values
# Remote Server Configuration
VITE_REMOTE_SERVER_HOST=127.0.0.1
VITE_REMOTE_SERVER_PORT=3001
# Chrome Extension Private Key (optional)
# 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,133 @@
# Persistent Connection Implementation Summary
## Overview
Modified the Chrome extension to implement persistent connection management that maintains connections until explicitly disconnected by the user.
## Key Changes Made
### 1. Enhanced RemoteServerClient (`utils/remote-server-client.ts`)
#### Connection State Persistence
- **Added persistent connection state management**:
- `persistentConnectionEnabled = true` by default
- `connectionStateKey = 'remoteServerConnectionState'` for storage
- Automatic state saving/loading to chrome.storage.local
#### New Methods Added
- `saveConnectionState()`: Saves connection state to chrome storage
- `loadConnectionState()`: Loads and restores connection state on startup
- `clearConnectionState()`: Clears saved state on manual disconnect
- `setPersistentConnection(enabled)`: Enable/disable persistent behavior
- `isPersistentConnectionEnabled()`: Get current persistence setting
#### Enhanced Reconnection Logic
- **Increased max reconnection attempts**: From 10 to 50 for persistent connections
- **Extended reconnection delays**: Up to 60 seconds for persistent connections
- **Smarter reconnection**: Only attempts reconnection for unexpected disconnections
- **Connection restoration**: Automatically restores connections within 24 hours
#### Connection Lifecycle Improvements
- **State saving on connect**: Automatically saves state when connection established
- **State clearing on disconnect**: Clears state only on manual disconnect
- **Robust error handling**: Better handling of connection timeouts and errors
### 2. Enhanced Background Script (`entrypoints/background/index.ts`)
#### Browser Event Listeners
- **Added `initBrowserEventListeners()`** function with listeners for:
- `chrome.runtime.onStartup`: Browser startup detection
- `chrome.runtime.onInstalled`: Extension install/update events
- `chrome.runtime.onSuspend`: Browser suspension events
- `chrome.tabs.onActivated`: Tab switch monitoring
- `chrome.windows.onFocusChanged`: Window focus monitoring
#### Connection Health Monitoring
- **Added `startConnectionHealthCheck()`**: 5-minute interval health checks
- **Periodic status logging**: Regular connection status verification
- **Proactive monitoring**: Detects and logs connection state changes
### 3. Enhanced Popup UI (`entrypoints/popup/App.vue`)
#### Visual Indicators
- **Persistent connection badge**: "🔗 Persistent: Active" indicator
- **Enhanced status text**: "Connected (Persistent)" vs "Disconnected - Click Connect for persistent connection"
- **Persistent info message**: "🔗 Connection will persist until manually disconnected"
#### CSS Styling
- **`.persistent-indicator`**: Styling for persistent connection elements
- **`.persistent-badge`**: Green gradient badge with shadow
- **`.persistent-info`**: Info box with green accent border
#### Status Text Updates
- **Clear messaging**: Emphasizes persistent nature of connections
- **User guidance**: Explains that connections persist until manual disconnect
## Technical Implementation Details
### Connection State Storage
```javascript
const state = {
wasConnected: boolean,
connectionTime: number,
serverUrl: string,
lastSaveTime: number
}
```
### Reconnection Strategy
- **Exponential backoff**: 5s, 10s, 20s, 40s, up to 60s intervals
- **Persistent attempts**: Up to 50 attempts for persistent connections
- **Smart restoration**: Only restores connections from last 24 hours
### Browser Event Handling
- **Tab switches**: Connection maintained across all tab operations
- **Window focus**: Connection persists during window focus changes
- **Browser suspension**: State saved before suspension, restored after
- **Extension updates**: Connection state preserved across updates
## Behavior Changes
### Before Implementation
- Manual connection required each session
- Connections might not survive browser events
- Limited reconnection attempts (10)
- No connection state persistence
### After Implementation
- **"Connect once, stay connected"** behavior
- **Connections persist across**:
- Popup open/close cycles
- Browser tab switches
- Window focus changes
- Extended idle periods
- Browser suspension/resume
- **Robust reconnection**: Up to 50 attempts with smart backoff
- **State restoration**: Automatic connection restoration after browser restart
- **Manual disconnect only**: Connections only terminate when explicitly disconnected
## User Experience Improvements
### Clear Visual Feedback
- Persistent connection status clearly indicated
- Real-time connection duration display
- Visual badges and indicators for connection state
### Predictable Behavior
- Users know connections will persist until manually disconnected
- No unexpected disconnections during tool operations
- Consistent behavior across browser lifecycle events
### Robust Connectivity
- Automatic reconnection when server becomes available
- Connection state restoration after browser restart
- Extended retry attempts for better reliability
## Testing
- Comprehensive test plan provided in `PERSISTENT_CONNECTION_TEST_PLAN.md`
- Covers all aspects of persistent connection behavior
- Includes verification points and success criteria
## Compatibility
- Maintains backward compatibility with existing MCP clients
- No breaking changes to tool execution or API
- Enhanced reliability without changing core functionality

View File

@@ -0,0 +1,170 @@
# Persistent Connection Test Plan
## Overview
This test plan verifies that the Chrome extension implements persistent connection management as requested:
- Connections remain active after manual connect until explicit disconnect
- Connections persist across popup open/close cycles
- Connections survive browser events (tab switches, window focus changes, idle periods)
- No automatic disconnection after tool operations
## Prerequisites
1. **Remote Server Running**: Start the remote server on `ws://localhost:3001/chrome`
```bash
cd app/remote-server
npm run dev
```
2. **Extension Loaded**: Load the built extension from `app/chrome-extension/.output/chrome-mv3`
## Test Cases
### Test 1: Basic Persistent Connection
**Objective**: Verify connection persists after popup close
**Steps**:
1. Open Chrome extension popup
2. Click "Connect" button
3. Verify connection status shows "Connected (Persistent)" with green checkmark
4. Note the persistent connection indicator: "🔗 Persistent: Active"
5. Close the popup window
6. Wait 30 seconds
7. Reopen the popup
**Expected Result**:
- Connection status still shows "Connected (Persistent)"
- Connection time continues counting from original connection
- No reconnection attempts logged
### Test 2: Connection Persistence Across Browser Events
**Objective**: Verify connection survives browser lifecycle events
**Steps**:
1. Establish connection (Test 1)
2. Switch between multiple tabs
3. Change window focus to different applications
4. Minimize/restore browser window
5. Open new browser windows
6. Check popup status after each event
**Expected Result**:
- Connection remains active throughout all events
- No disconnection or reconnection attempts
- Status consistently shows "Connected (Persistent)"
### Test 3: Tool Execution Without Disconnection
**Objective**: Verify connection persists during and after tool operations
**Steps**:
1. Establish connection
2. Use Cherry Studio or another MCP client to send tool requests:
- `chrome_navigate` to navigate to a website
- `chrome_screenshot` to take screenshots
- `chrome_extract_content` to extract page content
3. Execute multiple tool operations in sequence
4. Check connection status after each operation
**Expected Result**:
- All tool operations complete successfully
- Connection remains active after each operation
- No automatic disconnection after tool completion
### Test 4: Extended Idle Period Test
**Objective**: Verify connection survives long idle periods
**Steps**:
1. Establish connection
2. Leave browser idle for 30 minutes
3. Do not interact with the extension or browser
4. After 30 minutes, check popup status
5. Test tool functionality by sending a simple request
**Expected Result**:
- Connection remains active after idle period
- Tool operations work immediately without reconnection
- Connection time shows full duration including idle time
### Test 5: Manual Disconnect Only
**Objective**: Verify connection only terminates on explicit disconnect
**Steps**:
1. Establish connection
2. Perform various activities (tabs, tools, idle time)
3. Click "Disconnect" button in popup
4. Verify disconnection
5. Close and reopen popup
**Expected Result**:
- Connection terminates only when "Disconnect" is clicked
- After disconnect, status shows "Disconnected - Click Connect for persistent connection"
- Popup reopening shows disconnected state
- No automatic reconnection attempts
### Test 6: Browser Restart Connection Restoration
**Objective**: Verify connection state restoration after browser restart
**Steps**:
1. Establish connection
2. Close entire browser (all windows)
3. Restart browser
4. Open extension popup immediately
**Expected Result**:
- Extension attempts to restore previous connection
- If server is still running, connection is re-established automatically
- If successful, shows "Connected (Persistent)" status
### Test 7: Server Reconnection Behavior
**Objective**: Verify robust reconnection when server becomes unavailable
**Steps**:
1. Establish connection
2. Stop the remote server
3. Wait for connection loss detection
4. Restart the remote server
5. Monitor reconnection attempts
**Expected Result**:
- Extension detects connection loss
- Automatic reconnection attempts begin
- Connection is restored when server comes back online
- Persistent connection behavior resumes
## Verification Points
### UI Indicators
- ✅ Status shows "Connected (Persistent)" when connected
- ✅ Persistent badge shows "🔗 Persistent: Active"
- ✅ Info text: "🔗 Connection will persist until manually disconnected"
- ✅ Connection time continues counting accurately
- ✅ Disconnect button changes to "Disconnect" when connected
### Console Logs
Monitor browser console for these log messages:
- `Background: Remote server client initialized (not connected)`
- `Background: Browser event listeners initialized for connection persistence`
- `Background: Connection health check started (5-minute intervals)`
- `RemoteServerClient: Connection state saved`
- `Background: Tab switched to X, connection maintained`
- `Background: Window focus changed to X, connection maintained`
### Connection State Persistence
- Connection state is saved to chrome.storage.local
- State includes: wasConnected, connectionTime, serverUrl, lastSaveTime
- State is restored on extension startup if recent (within 24 hours)
## Success Criteria
All test cases pass with:
1. ✅ Connections persist until manual disconnect
2. ✅ No automatic disconnection after tool operations
3. ✅ Connections survive all browser lifecycle events
4. ✅ UI clearly indicates persistent connection status
5. ✅ Robust reconnection when server connectivity is restored
6. ✅ Connection state restoration after browser restart
## Troubleshooting
If tests fail, check:
1. Remote server is running on correct port (3001)
2. Extension has proper permissions
3. Browser console for error messages
4. Chrome storage for saved connection state
5. Network connectivity between extension and server

View File

@@ -0,0 +1,178 @@
# Chrome Extension User ID Guide
## Overview
The Chrome extension automatically generates and manages unique user IDs when connecting to the remote server. This guide explains how to access and use these user IDs.
## How User IDs Work
### 1. **Automatic Generation**
- Each Chrome extension instance generates a unique user ID in the format: `user_{timestamp}_{random}`
- Example: `user_1704067200000_abc123def456`
- User IDs are persistent across browser sessions (stored in chrome.storage.local)
### 2. **User ID Display in Popup**
When connected to the remote server, the popup will show:
- **User ID section** with the current user ID
- **Truncated display** for long IDs (shows first 8 and last 8 characters)
- **Copy button** (📋) to copy the full user ID to clipboard
- **Tooltip** showing the full user ID on hover
## Getting User ID Programmatically
### 1. **From Popup/Content Scripts**
```javascript
// Send message to background script to get user ID
const response = await chrome.runtime.sendMessage({ type: 'getCurrentUserId' });
if (response && response.success) {
const userId = response.userId;
console.log('Current User ID:', userId);
} else {
console.log('No user ID available or not connected');
}
```
### 2. **From Background Script**
```javascript
import { getRemoteServerClient } from './background/index';
// Get the remote server client instance
const client = getRemoteServerClient();
if (client) {
const userId = await client.getCurrentUserId();
console.log('Current User ID:', userId);
}
```
### 3. **Direct Storage Access**
```javascript
// Get user ID directly from chrome storage
const result = await chrome.storage.local.get(['chrome_extension_user_id']);
const userId = result.chrome_extension_user_id;
console.log('Stored User ID:', userId);
```
## User ID Lifecycle
### 1. **Generation**
- User ID is generated on first connection to remote server
- Stored in `chrome.storage.local` with key `chrome_extension_user_id`
- Persists across browser restarts and extension reloads
### 2. **Usage**
- Sent to remote server during connection handshake
- Used for session management and routing
- Enables multi-user support with session isolation
### 3. **Display**
- Shown in popup when connected to remote server
- Updates automatically when connection status changes
- Cleared from display when disconnected
## Remote Server Integration
### 1. **Connection Info**
When connecting, the extension sends:
```javascript
{
type: 'connection_info',
userId: 'user_1704067200000_abc123def456',
userAgent: navigator.userAgent,
timestamp: Date.now(),
extensionId: chrome.runtime.id
}
```
### 2. **Server-Side Access**
The remote server receives and uses the user ID for:
- Session management
- LiveKit room assignment (`mcp-chrome-user-{userId}`)
- Command routing
- User isolation
## Troubleshooting
### 1. **User ID Not Showing**
- Ensure you're connected to the remote server
- Check browser console for connection errors
- Verify remote server is running and accessible
### 2. **User ID Changes**
- User IDs should persist across sessions
- If changing frequently, check chrome.storage permissions
- Clear extension data to force new user ID generation
### 3. **Copy Function Not Working**
- Ensure clipboard permissions are granted
- Check for HTTPS context requirements
- Fallback: manually select and copy from tooltip
## API Reference
### Background Script Messages
#### `getCurrentUserId`
```javascript
// Request
{ type: 'getCurrentUserId' }
// Response
{
success: true,
userId: 'user_1704067200000_abc123def456'
}
// or
{
success: false,
error: 'Remote server client not initialized'
}
```
### Storage Keys
#### `chrome_extension_user_id`
- **Type**: String
- **Format**: `user_{timestamp}_{random}`
- **Persistence**: Permanent (until extension data cleared)
- **Access**: chrome.storage.local
## Best Practices
1. **Always check connection status** before requesting user ID
2. **Handle null/undefined user IDs** gracefully
3. **Use the background script API** for reliable access
4. **Don't hardcode user IDs** - they should be dynamic
5. **Respect user privacy** - user IDs are anonymous but unique
## Example Implementation
```javascript
// Complete example of getting and using user ID
async function handleUserIdExample() {
try {
// Get current user ID
const response = await chrome.runtime.sendMessage({
type: 'getCurrentUserId'
});
if (response && response.success && response.userId) {
console.log('✅ User ID:', response.userId);
// Use the user ID for your application logic
await processUserSpecificData(response.userId);
} else {
console.log('❌ No user ID available');
console.log('Make sure you are connected to the remote server');
}
} catch (error) {
console.error('Failed to get user ID:', error);
}
}
async function processUserSpecificData(userId) {
// Your application logic here
console.log(`Processing data for user: ${userId}`);
}
```

View File

@@ -442,5 +442,61 @@
"pagesUnit": {
"message": "pages",
"description": "Pages count unit"
},
"remoteServerConfigLabel": {
"message": "Remote Server Configuration",
"description": "Main section header for remote server settings"
},
"remoteServerStatusLabel": {
"message": "Remote Server Status",
"description": "Remote server status label"
},
"remoteMcpServerConfigLabel": {
"message": "Remote MCP Server Configuration",
"description": "Remote MCP server config label"
},
"serverEndpointLabel": {
"message": "Server Endpoint",
"description": "Server endpoint label"
},
"reconnectAttemptsLabel": {
"message": "Reconnect Attempts",
"description": "Reconnect attempts label"
},
"connectionTimeLabel": {
"message": "Connected For",
"description": "Connection duration label"
},
"remoteServerConnectedStatus": {
"message": "Connected to Remote Server",
"description": "Remote server connected status"
},
"remoteServerConnectingStatus": {
"message": "Connecting to Remote Server...",
"description": "Remote server connecting status"
},
"remoteServerDisconnectedStatus": {
"message": "Disconnected from Remote Server",
"description": "Remote server disconnected status"
},
"remoteServerErrorStatus": {
"message": "Remote Server Error",
"description": "Remote server error status"
},
"copiedButton": {
"message": "✅ Copied!",
"description": "Config copied button text"
},
"copyFailedButton": {
"message": "❌ Copy Failed",
"description": "Config copy failed button text"
},
"recommendedLabel": {
"message": "Recommended",
"description": "Recommended configuration badge"
},
"alternativeLabel": {
"message": "Alternative",
"description": "Alternative configuration badge"
}
}

View File

@@ -442,5 +442,61 @@
"pagesUnit": {
"message": "页",
"description": "页面计数单位"
},
"copiedButton": {
"message": "✅ 已复制!",
"description": "配置复制按钮文本"
},
"copyFailedButton": {
"message": "❌ 复制失败",
"description": "配置复制失败按钮文本"
},
"recommendedLabel": {
"message": "推荐",
"description": "推荐配置标识"
},
"alternativeLabel": {
"message": "备选",
"description": "备选配置标识"
},
"remoteServerConfigLabel": {
"message": "远程服务器配置",
"description": "远程服务器设置的主要节标题"
},
"remoteServerStatusLabel": {
"message": "远程服务器状态",
"description": "远程服务器状态标签"
},
"remoteMcpServerConfigLabel": {
"message": "远程 MCP 服务器配置",
"description": "远程 MCP 服务器配置标签"
},
"serverEndpointLabel": {
"message": "服务器端点",
"description": "服务器端点标签"
},
"reconnectAttemptsLabel": {
"message": "重连尝试",
"description": "重连尝试次数标签"
},
"connectionTimeLabel": {
"message": "连接时长",
"description": "连接持续时间标签"
},
"remoteServerConnectedStatus": {
"message": "已连接到远程服务器",
"description": "远程服务器已连接状态"
},
"remoteServerConnectingStatus": {
"message": "正在连接远程服务器...",
"description": "远程服务器连接中状态"
},
"remoteServerDisconnectedStatus": {
"message": "已断开远程服务器连接",
"description": "远程服务器已断开状态"
},
"remoteServerErrorStatus": {
"message": "远程服务器错误",
"description": "远程服务器错误状态"
}
}

View File

@@ -17,11 +17,13 @@ export const ICONS = {
// Timeouts and Delays (in milliseconds)
export const TIMEOUTS = {
DEFAULT_WAIT: 1000,
NETWORK_CAPTURE_MAX: 30000,
NETWORK_CAPTURE_IDLE: 3000,
NETWORK_CAPTURE_MAX: 60000, // Increased from 30000
NETWORK_CAPTURE_IDLE: 5000, // Increased from 3000
SCREENSHOT_DELAY: 100,
KEYBOARD_DELAY: 50,
CLICK_DELAY: 100,
REMOTE_SERVER_CONNECTION: 45000, // Increased from 30000ms to 45000ms for more reliable connections
TOOL_EXECUTION: 60000, // New timeout for tool execution
} as const;
// Limits and Thresholds

View File

@@ -0,0 +1,35 @@
/**
* Environment Configuration
* Centralized environment variable handling for Chrome Extension
*/
// Get environment variables with fallbacks
const REMOTE_SERVER_HOST = import.meta.env.VITE_REMOTE_SERVER_HOST || '127.0.0.1';
const REMOTE_SERVER_PORT = import.meta.env.VITE_REMOTE_SERVER_PORT || '3001';
// Debug logging for environment variables
console.log('Environment Config Loaded:', {
VITE_REMOTE_SERVER_HOST: import.meta.env.VITE_REMOTE_SERVER_HOST,
VITE_REMOTE_SERVER_PORT: import.meta.env.VITE_REMOTE_SERVER_PORT,
REMOTE_SERVER_HOST,
REMOTE_SERVER_PORT,
});
// Remote Server Configuration
export const REMOTE_SERVER_CONFIG = {
HOST: REMOTE_SERVER_HOST,
PORT: REMOTE_SERVER_PORT,
WS_URL: `ws://${REMOTE_SERVER_HOST}:${REMOTE_SERVER_PORT}/chrome`,
HTTP_URL: `http://${REMOTE_SERVER_HOST}:${REMOTE_SERVER_PORT}/mcp`,
} as const;
// Default connection settings
export const DEFAULT_CONNECTION_CONFIG = {
serverUrl: REMOTE_SERVER_CONFIG.WS_URL,
reconnectInterval: 3000, // Reduced from 5000ms to 3000ms for faster reconnection
maxReconnectAttempts: 999999, // Effectively unlimited for persistent connections
} as const;
// Export individual values for backward compatibility
export const DEFAULT_SERVER_URL = REMOTE_SERVER_CONFIG.WS_URL;
export const DEFAULT_HTTP_URL = REMOTE_SERVER_CONFIG.HTTP_URL;

View File

@@ -22,3 +22,15 @@ export const createErrorResponse = (
isError: true,
};
};
export const createSuccessResponse = (data: any): ToolResult => {
return {
content: [
{
type: 'text',
text: typeof data === 'string' ? data : JSON.stringify(data, null, 2),
},
],
isError: false,
};
};

View File

@@ -1,38 +1,369 @@
import { initNativeHostListener } from './native-host';
import {
initSemanticSimilarityListener,
initializeSemanticEngineIfCached,
} from './semantic-similarity';
// Native messaging removed - using remote server only
// import { initNativeHostListener } from './native-host';
// Temporarily disable semantic similarity to focus on connection issues
// import {
// initSemanticSimilarityListener,
// initializeSemanticEngineIfCached,
// } from './semantic-similarity';
import { initStorageManagerListener } from './storage-manager';
import { cleanupModelCache } from '@/utils/semantic-similarity-engine';
import { RemoteServerClient } from '@/utils/remote-server-client';
import { DEFAULT_CONNECTION_CONFIG } from '@/common/env-config';
import { handleCallTool } from './tools';
// Global remote server client instance
let remoteServerClient: RemoteServerClient | null = null;
/**
* Background script entry point
* Initializes all background services and listeners
*/
export default defineBackground(() => {
// Initialize core services
initNativeHostListener();
initSemanticSimilarityListener();
// Initialize remote server client first (prioritize over native messaging)
initRemoteServerClient();
// Initialize core services (native messaging removed)
// initNativeHostListener();
// initSemanticSimilarityListener();
initStorageManagerListener();
// Initialize browser event listeners for connection persistence
initBrowserEventListeners();
// 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);
});
// 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);
});
});
/**
* Initialize remote server client (without auto-connecting)
*/
function initRemoteServerClient() {
try {
remoteServerClient = new RemoteServerClient({
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
reconnectInterval: DEFAULT_CONNECTION_CONFIG.reconnectInterval,
maxReconnectAttempts: 50, // Increased for better reliability
});
console.log('Background: Remote server client initialized (not connected)');
console.log('Background: Use popup to manually connect to remote server');
} catch (error) {
console.error('Background: Failed to initialize remote server client:', error);
}
}
/**
* Get the remote server client instance
*/
export function getRemoteServerClient(): RemoteServerClient | null {
return remoteServerClient;
}
/**
* Initialize browser event listeners for connection persistence
*/
function initBrowserEventListeners() {
// Listen for browser startup events
chrome.runtime.onStartup.addListener(() => {
console.log('Background: Browser startup detected. Manual connection required via popup.');
if (remoteServerClient) {
console.log('Background: Remote server client ready for manual connection');
}
});
// Listen for extension installation/update events
chrome.runtime.onInstalled.addListener((details) => {
console.log('Background: Extension installed/updated:', details.reason);
if (details.reason === 'update') {
console.log('Background: Extension updated, manual connection required');
}
});
// Listen for browser suspension/resume events (Chrome specific)
if (chrome.runtime.onSuspend) {
chrome.runtime.onSuspend.addListener(() => {
console.log('Background: Browser suspending, connection state saved');
// Connection state is automatically saved when connected
});
}
if (chrome.runtime.onSuspendCanceled) {
chrome.runtime.onSuspendCanceled.addListener(() => {
console.log('Background: Browser suspend canceled, maintaining connection');
});
}
// Monitor tab events to ensure connection persists across tab operations
chrome.tabs.onActivated.addListener((activeInfo) => {
// Connection should persist regardless of tab switches
if (remoteServerClient && remoteServerClient.isConnected()) {
console.log(`Background: Tab switched to ${activeInfo.tabId}, connection maintained`);
}
});
// Monitor window events
chrome.windows.onFocusChanged.addListener((windowId) => {
// Connection should persist regardless of window focus changes
if (
remoteServerClient &&
remoteServerClient.isConnected() &&
windowId !== chrome.windows.WINDOW_ID_NONE
) {
console.log(`Background: Window focus changed to ${windowId}, connection maintained`);
}
});
console.log('Background: Browser event listeners initialized for connection persistence');
// Start periodic connection health check
startConnectionHealthCheck();
}
/**
* Start periodic connection health check to maintain persistent connections
*/
function startConnectionHealthCheck() {
// Check connection health every 5 minutes (for monitoring only, no auto-reconnection)
setInterval(
() => {
if (remoteServerClient) {
const isConnected = remoteServerClient.isConnected();
console.log(`Background: Connection health check - Connected: ${isConnected}`);
if (!isConnected) {
console.log('Background: Connection lost. Use popup to manually reconnect.');
// No automatic reconnection - user must manually reconnect via popup
}
}
},
5 * 60 * 1000,
); // 5 minutes
console.log(
'Background: Connection health check started (monitoring only, no auto-reconnection)',
);
}
/**
* Handle messages from popup for remote server control
*/
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'getRemoteServerStatus') {
const status = remoteServerClient?.getStatus() || {
connected: false,
connecting: false,
reconnectAttempts: 0,
connectionTime: undefined,
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
};
sendResponse(status);
return true;
}
if (message.type === 'connectRemoteServer') {
if (!remoteServerClient) {
sendResponse({ success: false, error: 'Remote server client not initialized' });
return true;
}
if (remoteServerClient.isConnected()) {
sendResponse({ success: true, message: 'Already connected' });
return true;
}
console.log('Background: Attempting to connect to remote server...');
remoteServerClient
.connect()
.then(() => {
console.log('Background: Successfully connected to remote server');
sendResponse({ success: true });
})
.catch((error) => {
console.error('Background: Failed to connect to remote server:', error);
sendResponse({ success: false, error: error.message });
});
return true;
}
if (message.type === 'disconnectRemoteServer') {
if (!remoteServerClient) {
sendResponse({ success: false, error: 'Remote server client not initialized' });
return true;
}
console.log('Background: Disconnecting from remote server...');
try {
remoteServerClient.disconnect();
console.log('Background: Successfully disconnected from remote server');
sendResponse({ success: true });
} catch (error) {
console.error('Background: Error during disconnect:', error);
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Disconnect failed',
});
}
return true;
}
if (message.type === 'restoreRemoteConnection') {
if (!remoteServerClient) {
sendResponse({ success: false, error: 'Remote server client not initialized' });
return true;
}
if (remoteServerClient.isConnected()) {
sendResponse({ success: true, message: 'Already connected' });
return true;
}
console.log('Background: Attempting to restore previous connection...');
remoteServerClient
.restoreConnectionFromState()
.then((restored) => {
if (restored) {
console.log('Background: Successfully restored previous connection');
sendResponse({ success: true });
} else {
console.log('Background: No previous connection to restore');
sendResponse({ success: false, error: 'No previous connection found' });
}
})
.catch((error) => {
console.error('Background: Failed to restore previous connection:', error);
sendResponse({ success: false, error: error.message });
});
return true;
}
if (message.type === 'getCurrentUserId') {
if (!remoteServerClient) {
sendResponse({ success: false, error: 'Remote server client not initialized' });
return true;
}
remoteServerClient
.getCurrentUserId()
.then((userId) => {
sendResponse({ success: true, userId });
})
.catch((error) => {
console.error('Background: Failed to get current user ID:', error);
sendResponse({ success: false, error: error.message });
});
return true;
}
if (message.type === 'callTool') {
handleCallTool({ name: message.toolName, args: message.params })
.then((result) => {
sendResponse(result);
})
.catch((error) => {
sendResponse({ error: error.message });
});
return true;
}
if (message.type === 'injectUserIdHelper') {
injectUserIdHelper(message.tabId)
.then((result) => {
sendResponse(result);
})
.catch((error) => {
sendResponse({ success: false, error: error.message });
});
return true;
}
});
/**
* Inject user ID helper script into a specific tab
*/
async function injectUserIdHelper(tabId?: number): Promise<{ success: boolean; message: string }> {
try {
let targetTabId = tabId;
// If no tab ID provided, use the active tab
if (!targetTabId) {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
targetTabId = tabs[0].id;
}
// Inject the user ID helper script
await chrome.scripting.executeScript({
target: { tabId: targetTabId },
files: ['inject-scripts/user-id-helper.js'],
});
// Get current user ID and inject it
if (remoteServerClient) {
const userId = await remoteServerClient.getCurrentUserId();
if (userId) {
// Inject the user ID into the page
await chrome.scripting.executeScript({
target: { tabId: targetTabId },
func: (userId) => {
// Make user ID available globally
(window as any).chromeExtensionUserId = userId;
// Store in sessionStorage
try {
sessionStorage.setItem('chromeExtensionUserId', userId);
} catch (e) {
// Ignore storage errors
}
// Dispatch event for pages waiting for user ID
window.dispatchEvent(
new CustomEvent('chromeExtensionUserIdReady', {
detail: { userId: userId },
}),
);
console.log('Chrome Extension User ID injected:', userId);
},
args: [userId],
});
return {
success: true,
message: `User ID helper injected into tab ${targetTabId} with user ID: ${userId}`,
};
} else {
return {
success: true,
message: `User ID helper injected into tab ${targetTabId} but no user ID available (not connected)`,
};
}
} else {
return {
success: true,
message: `User ID helper injected into tab ${targetTabId} but remote server client not initialized`,
};
}
} catch (error) {
console.error('Failed to inject user ID helper:', error);
throw error;
}
}

View File

@@ -2,18 +2,66 @@ import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
// Default window dimensions
// Default window dimensions - optimized for automation tools
const DEFAULT_WINDOW_WIDTH = 1280;
const DEFAULT_WINDOW_HEIGHT = 720;
interface NavigateToolParams {
url?: string;
newWindow?: boolean;
backgroundPage?: boolean;
width?: number;
height?: number;
refresh?: boolean;
}
/**
* Helper function to create automation-friendly background windows
* Ensures proper dimensions and timing for web automation tools
*/
async function createAutomationFriendlyBackgroundWindow(
url: string,
width: number,
height: number,
): Promise<chrome.windows.Window | null> {
try {
console.log(`Creating automation-friendly background window: ${width}x${height} for ${url}`);
// Create window with optimal settings for automation
const window = await chrome.windows.create({
url: url,
width: width,
height: height,
focused: false, // Don't steal focus from user
state: chrome.windows.WindowState.NORMAL, // Start in normal state
type: 'normal', // Normal window type for full automation compatibility
// Ensure window is created with proper viewport
left: 0, // Position consistently for automation
top: 0,
});
if (window && window.id !== undefined) {
// Wait for window to be properly established
await new Promise((resolve) => setTimeout(resolve, 1500));
// Verify window still exists and has correct dimensions
const windowInfo = await chrome.windows.get(window.id);
if (windowInfo && windowInfo.width === width && windowInfo.height === height) {
console.log(`Background window ${window.id} established with correct dimensions`);
return window;
} else {
console.warn(`Window ${window.id} dimensions may not be correct`);
return window; // Return anyway, might still work
}
}
return null;
} catch (error) {
console.error('Failed to create automation-friendly background window:', error);
return null;
}
}
/**
* Tool for navigating to URLs in browser tabs or windows
*/
@@ -21,11 +69,26 @@ class NavigateTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.NAVIGATE;
async execute(args: NavigateToolParams): Promise<ToolResult> {
// Check if backgroundPage was explicitly provided, if not, check user settings
let backgroundPage = args.backgroundPage;
if (backgroundPage === undefined) {
try {
const result = await chrome.storage.local.get(['openUrlsInBackground']);
// Default to true for background windows (changed from false to true)
backgroundPage =
result.openUrlsInBackground !== undefined ? result.openUrlsInBackground : true;
console.log(`Using stored background page preference: ${backgroundPage}`);
} catch (error) {
console.warn('Failed to load background page preference, using default (true):', error);
backgroundPage = true; // Default to background windows
}
}
const { newWindow = false, width, height, url, refresh = false } = args;
console.log(
`Attempting to ${refresh ? 'refresh current tab' : `open URL: ${url}`} with options:`,
args,
{ ...args, backgroundPage },
);
try {
@@ -121,7 +184,83 @@ class NavigateTool extends BaseBrowserToolExecutor {
}
}
// 2. If URL is not already open, decide how to open it based on options
// 2. Handle background page option
if (backgroundPage) {
console.log(
'Opening URL in background page using full-size window that will be minimized.',
);
const windowWidth = typeof width === 'number' ? width : DEFAULT_WINDOW_WIDTH;
const windowHeight = typeof height === 'number' ? height : DEFAULT_WINDOW_HEIGHT;
// Create automation-friendly background window
const backgroundWindow = await createAutomationFriendlyBackgroundWindow(
url!,
windowWidth,
windowHeight,
);
if (backgroundWindow && backgroundWindow.id !== undefined) {
console.log(
`Background window created with ID: ${backgroundWindow.id}, dimensions: ${windowWidth}x${windowHeight}`,
);
try {
// Verify window still exists before minimizing
const windowInfo = await chrome.windows.get(backgroundWindow.id);
if (windowInfo) {
console.log(
`Minimizing window ${backgroundWindow.id} while preserving automation accessibility`,
);
// Now minimize the window to keep it in background while maintaining automation accessibility
await chrome.windows.update(backgroundWindow.id, {
state: chrome.windows.WindowState.MINIMIZED,
});
console.log(
`URL opened in background Window ID: ${backgroundWindow.id} (${windowWidth}x${windowHeight} then minimized)`,
);
}
} catch (error) {
console.warn(`Failed to minimize window ${backgroundWindow.id}:`, error);
// Continue anyway as the window was created successfully
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message:
'Opened URL in background page (full-size window then minimized for automation compatibility)',
windowId: backgroundWindow.id,
width: windowWidth,
height: windowHeight,
tabs: backgroundWindow.tabs
? backgroundWindow.tabs.map((tab) => ({
tabId: tab.id,
url: tab.url,
}))
: [],
automationReady: true,
minimized: true,
dimensions: `${windowWidth}x${windowHeight}`,
}),
},
],
isError: false,
};
} else {
console.error('Failed to create automation-friendly background window');
return createErrorResponse(
'Failed to create background window with proper automation compatibility',
);
}
}
// 3. 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) {

View File

@@ -0,0 +1,184 @@
import { BaseBrowserToolExecutor } from '../base-browser';
import { createErrorResponse, createSuccessResponse } from '../../../../common/tool-handler';
import { ERROR_MESSAGES } from '../../../../common/constants';
export class EnhancedSearchTool extends BaseBrowserToolExecutor {
async chromeSearchGoogle(args: {
query: string;
openGoogle?: boolean;
extractResults?: boolean;
maxResults?: number;
}) {
const { query, openGoogle = true, extractResults = true, maxResults = 10 } = args;
try {
// Step 1: Navigate to Google if requested
if (openGoogle) {
await this.navigateToGoogle();
await this.sleep(3000); // Wait for page to load
}
// Step 2: Find and fill search box
const searchSuccess = await this.performGoogleSearch(query);
if (!searchSuccess) {
return createErrorResponse(
'Failed to perform Google search - could not find or interact with search box',
);
}
// Step 3: Wait for results to load
await this.sleep(3000);
// Step 4: Extract results if requested
if (extractResults) {
const results = await this.extractSearchResults(maxResults);
return createSuccessResponse({
query,
searchCompleted: true,
resultsExtracted: true,
results,
});
}
return createSuccessResponse({
query,
searchCompleted: true,
resultsExtracted: false,
message: 'Google search completed successfully',
});
} catch (error) {
return createErrorResponse(
`Error performing Google search: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
async chromeSubmitForm(args: {
formSelector?: string;
inputSelector?: string;
submitMethod?: 'enter' | 'button' | 'auto';
}) {
const { formSelector = 'form', inputSelector, submitMethod = 'auto' } = args;
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
}
const tabId = tabs[0].id;
// Inject form submission script
await this.injectContentScript(tabId, ['inject-scripts/form-submit-helper.js']);
const result = await this.sendMessageToTab(tabId, {
action: 'submitForm',
formSelector,
inputSelector,
submitMethod,
});
if (result.error) {
return createErrorResponse(result.error);
}
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(
`Error submitting form: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
private async navigateToGoogle(): Promise<void> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
await chrome.tabs.update(tabs[0].id, { url: 'https://www.google.com' });
}
private async performGoogleSearch(query: string): Promise<boolean> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
const tabId = tabs[0].id;
// Enhanced search box selectors
const searchSelectors = [
'#APjFqb', // Main Google search box ID
'textarea[name="q"]', // Google search textarea
'input[name="q"]', // Google search input (fallback)
'[role="combobox"]', // Role-based selector
'.gLFyf', // Google search box class
'textarea[aria-label*="Search"]', // Aria-label based
'[title*="Search"]', // Title attribute
'.gsfi', // Google search field input class
'#lst-ib', // Alternative Google search ID
'input[type="search"]', // Generic search input
'textarea[role="combobox"]', // Textarea with combobox role
];
// Inject search helper script
await this.injectContentScript(tabId, ['inject-scripts/enhanced-search-helper.js']);
for (const selector of searchSelectors) {
try {
const result = await this.sendMessageToTab(tabId, {
action: 'performGoogleSearch',
selector,
query,
});
if (result.success) {
return true;
}
} catch (error) {
console.debug(`Search selector ${selector} failed:`, error);
continue;
}
}
return false;
}
private async extractSearchResults(maxResults: number): Promise<any[]> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
const tabId = tabs[0].id;
const result = await this.sendMessageToTab(tabId, {
action: 'extractSearchResults',
maxResults,
});
return result.results || [];
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Export tool instances
export const searchGoogleTool = new (class extends EnhancedSearchTool {
name = 'chrome_search_google';
async execute(args: any) {
return await this.chromeSearchGoogle(args);
}
})();
export const submitFormTool = new (class extends EnhancedSearchTool {
name = 'chrome_submit_form';
async execute(args: any) {
return await this.chromeSubmitForm(args);
}
})();

View File

@@ -12,3 +12,4 @@ export { historyTool } from './history';
export { bookmarkSearchTool, bookmarkAddTool, bookmarkDeleteTool } from './bookmark';
export { injectScriptTool, sendCommandToInjectScriptTool } from './inject-script';
export { consoleTool } from './console';
export { searchGoogleTool, submitFormTool } from './enhanced-search';

View File

@@ -134,10 +134,13 @@ class NetworkDebuggerStartTool extends BaseBrowserToolExecutor {
}
NetworkDebuggerStartTool.instance = this;
chrome.debugger.onEvent.addListener(this.handleDebuggerEvent.bind(this));
chrome.debugger.onDetach.addListener(this.handleDebuggerDetach.bind(this));
chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this));
chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this));
// Only add listeners if chrome APIs are available (not during build)
if (typeof chrome !== 'undefined' && chrome.debugger?.onEvent) {
chrome.debugger.onEvent.addListener(this.handleDebuggerEvent.bind(this));
chrome.debugger.onDetach.addListener(this.handleDebuggerDetach.bind(this));
chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this));
chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this));
}
}
private handleTabRemoved(tabId: number) {

View File

@@ -3,7 +3,7 @@ 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
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 60000; // For sending a single request via content script - increased from 30000
interface NetworkRequestToolParams {
url: string; // URL is always required

View File

@@ -17,15 +17,31 @@ export interface ToolCallParam {
* Handle tool execution
*/
export const handleCallTool = async (param: ToolCallParam) => {
console.log('🛠️ [Tool Handler] Executing tool:', {
toolName: param.name,
hasArgs: !!param.args,
availableTools: Array.from(toolsMap.keys()),
args: param.args,
});
const tool = toolsMap.get(param.name);
if (!tool) {
console.error('🛠️ [Tool Handler] Tool not found:', param.name);
return createErrorResponse(`Tool ${param.name} not found`);
}
try {
return await tool.execute(param.args);
console.log('🛠️ [Tool Handler] Starting tool execution for:', param.name);
const result = await tool.execute(param.args);
console.log('🛠️ [Tool Handler] Tool execution completed:', {
toolName: param.name,
hasResult: !!result,
isError: result?.isError,
result,
});
return result;
} catch (error) {
console.error(`Tool execution failed for ${param.name}:`, error);
console.error(`🛠️ [Tool Handler] Tool execution failed for ${param.name}:`, error);
return createErrorResponse(
error instanceof Error ? error.message : ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
);

View File

@@ -1,4 +1,44 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {},
matches: ['<all_urls>'],
main() {
// Content script is now properly configured for all URLs
// The actual functionality is handled by dynamically injected scripts
// This ensures the content script context is available for communication
console.log('Chrome MCP Extension content script loaded');
// Make user ID available globally on any page
setupUserIdAccess();
},
});
async function setupUserIdAccess() {
try {
// Get user ID from background script
const response = await chrome.runtime.sendMessage({ type: 'getCurrentUserId' });
if (response && response.success && response.userId) {
// Make user ID available globally on the page
(window as any).chromeExtensionUserId = response.userId;
// Also store in a custom event for pages that need it
window.dispatchEvent(
new CustomEvent('chromeExtensionUserIdReady', {
detail: { userId: response.userId },
}),
);
// Store in sessionStorage for easy access
try {
sessionStorage.setItem('chromeExtensionUserId', response.userId);
} catch (e) {
// Ignore storage errors (some sites block this)
}
console.log('Chrome Extension User ID available:', response.userId);
} else {
console.log('Chrome Extension: No user ID available (not connected to server)');
}
} catch (error) {
console.error('Chrome Extension: Failed to get user ID:', error);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -244,3 +244,348 @@ select:focus {
transition-duration: 0.01ms !important;
}
}
/* Enhanced Connection Status Display */
.connection-status-display {
margin: var(--spacing-lg) 0;
padding: var(--spacing-lg);
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.status-indicator {
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
}
.status-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
flex-shrink: 0;
transition: all var(--transition-normal);
}
.status-icon.status-connected {
background: var(--success-color);
color: white;
}
.status-icon.status-connecting {
background: var(--warning-color);
color: white;
}
.status-icon.status-disconnected {
background: var(--text-muted);
color: white;
}
.status-icon.status-error {
background: var(--error-color);
color: white;
}
.status-details {
flex: 1;
min-width: 0;
}
.status-text-primary {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.status-error {
font-size: 14px;
color: var(--error-color);
margin-bottom: var(--spacing-xs);
word-wrap: break-word;
}
.status-info {
font-size: 12px;
color: var(--text-muted);
margin-bottom: var(--spacing-xs);
}
.status-info:last-child {
margin-bottom: 0;
}
/* Loading Spinner */
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Enhanced Connect Button States */
.connect-button--connected {
background: var(--success-color) !important;
}
.connect-button--connected:hover:not(:disabled) {
background: #38a169 !important;
}
.connect-button--connecting {
background: var(--warning-color) !important;
position: relative;
overflow: hidden;
}
.connect-button--connecting::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: shimmer 1.5s infinite;
}
.connect-button--error {
background: var(--error-color) !important;
}
.connect-button--error:hover:not(:disabled) {
background: #e53e3e !important;
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* Error Actions and Help */
.error-actions {
margin-top: var(--spacing-sm);
display: flex;
gap: var(--spacing-sm);
}
.retry-button,
.help-button {
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast);
}
.retry-button:hover:not(:disabled) {
background: var(--info-color);
color: white;
border-color: var(--info-color);
}
.help-button:hover {
background: var(--warning-color);
color: white;
border-color: var(--warning-color);
}
.retry-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.connection-help {
margin-top: var(--spacing-md);
padding: var(--spacing-md);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
animation: slideDown var(--transition-normal);
}
.help-content h4 {
margin: 0 0 var(--spacing-sm) 0;
color: var(--text-primary);
font-size: 14px;
}
.help-content ul {
margin: 0 0 var(--spacing-md) 0;
padding-left: var(--spacing-lg);
color: var(--text-secondary);
font-size: 12px;
line-height: 1.5;
}
.help-content li {
margin-bottom: var(--spacing-xs);
}
.close-help-button {
padding: var(--spacing-xs) var(--spacing-md);
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--radius-sm);
font-size: 12px;
cursor: pointer;
transition: background var(--transition-fast);
}
.close-help-button:hover {
background: var(--primary-dark);
}
/* Connection Settings */
.connection-settings {
margin: var(--spacing-md) 0;
padding: var(--spacing-md);
background: var(--bg-tertiary);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
}
.setting-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
cursor: pointer;
user-select: none;
}
.setting-checkbox {
width: 16px;
height: 16px;
accent-color: var(--primary-color);
cursor: pointer;
}
.setting-label {
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
}
/* Advanced Settings */
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-light);
}
.settings-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.settings-toggle {
padding: var(--spacing-xs);
background: none;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast);
min-width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.settings-toggle:hover {
background: var(--bg-secondary);
border-color: var(--primary-color);
color: var(--primary-color);
}
.advanced-settings {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-light);
animation: slideDown var(--transition-normal);
}
.setting-group {
margin-bottom: var(--spacing-md);
}
.setting-label-block {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.setting-input {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-primary);
font-size: 12px;
transition: all var(--transition-fast);
}
.setting-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.setting-input:invalid {
border-color: var(--error-color);
}
.setting-actions {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-light);
}
.reset-button {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--warning-color);
color: white;
border: none;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background var(--transition-fast);
}
.reset-button:hover {
background: #d69e2e;
}

View File

@@ -0,0 +1,560 @@
/* eslint-disable */
// enhanced-search-helper.js
// Enhanced search automation with multiple submission methods
if (window.__ENHANCED_SEARCH_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__ENHANCED_SEARCH_HELPER_INITIALIZED__ = true;
/**
* Perform Google search with enhanced reliability
* @param {string} selector - CSS selector for the search box
* @param {string} query - Search query
* @returns {Promise<Object>} - Result of the search operation
*/
async function performGoogleSearch(selector, query) {
try {
console.log(`🔍 Attempting Google search with selector: ${selector}, query: ${query}`);
// Find the search element
const searchElement = document.querySelector(selector);
if (!searchElement) {
return {
success: false,
error: `Search element with selector "${selector}" not found`,
};
}
// Focus and clear the search box
searchElement.focus();
await sleep(200);
// Clear existing content
searchElement.select();
await sleep(100);
// Fill the search box
searchElement.value = query;
// Trigger input events to ensure the page recognizes the input
searchElement.dispatchEvent(new Event('input', { bubbles: true }));
searchElement.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(500);
// Try multiple submission methods
const submissionSuccess = await submitGoogleSearch(searchElement, query);
if (submissionSuccess) {
console.log(`✅ Google search submitted successfully using selector: ${selector}`);
return {
success: true,
selector,
query,
method: submissionSuccess.method,
};
} else {
return {
success: false,
error: 'All submission methods failed',
};
}
} catch (error) {
console.error('Error in performGoogleSearch:', error);
return {
success: false,
error: `Unexpected error: ${error.message}`,
};
}
}
/**
* Try multiple methods to submit Google search
* @param {Element} searchElement - The search input element
* @param {string} query - Search query
* @returns {Promise<Object|null>} - Success result or null
*/
async function submitGoogleSearch(searchElement, query) {
const methods = [
{
name: 'enter_key',
action: async () => {
console.log('🔄 Method 1: Trying Enter key');
searchElement.focus();
await sleep(200);
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
});
searchElement.dispatchEvent(enterEvent);
await sleep(1000);
// Check if search was successful
if (await checkSearchResultsLoaded()) {
return { method: 'enter_key' };
}
return null;
},
},
{
name: 'search_button',
action: async () => {
console.log('🔄 Method 2: Trying search button');
const buttonSelectors = [
'input[value*="Google Search"]',
'button[aria-label*="Google Search"]',
'input[type="submit"][value*="Google Search"]',
'.gNO89b', // Google Search button class
'center input[type="submit"]:first-of-type',
'button[type="submit"]',
'[role="button"][aria-label*="search"]',
'.Tg7LZd',
];
for (const buttonSelector of buttonSelectors) {
try {
const button = document.querySelector(buttonSelector);
if (button) {
button.click();
await sleep(1000);
if (await checkSearchResultsLoaded()) {
return { method: 'search_button', selector: buttonSelector };
}
}
} catch (e) {
continue;
}
}
return null;
},
},
{
name: 'form_submit',
action: async () => {
console.log('🔄 Method 3: Trying form submission');
const form = searchElement.closest('form');
if (form) {
form.submit();
await sleep(1000);
if (await checkSearchResultsLoaded()) {
return { method: 'form_submit' };
}
}
return null;
},
},
{
name: 'double_enter',
action: async () => {
console.log('🔄 Method 4: Trying double Enter');
searchElement.focus();
await sleep(200);
// First Enter
const enterEvent1 = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
});
searchElement.dispatchEvent(enterEvent1);
await sleep(300);
// Second Enter
const enterEvent2 = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
});
searchElement.dispatchEvent(enterEvent2);
await sleep(1000);
if (await checkSearchResultsLoaded()) {
return { method: 'double_enter' };
}
return null;
},
},
];
for (const method of methods) {
try {
const result = await method.action();
if (result) {
console.log(`✅ Submission method "${method.name}" successful`);
return result;
}
} catch (error) {
console.debug(`Submission method "${method.name}" failed:`, error);
continue;
}
}
console.warn('❌ All submission methods failed');
return null;
}
/**
* Check if Google search results have loaded
* @returns {Promise<boolean>}
*/
async function checkSearchResultsLoaded() {
const resultIndicators = [
'#search', // Main search results container
'#rso', // Results container
'.g', // Individual result
'.tF2Cxc', // Modern Google result container
'#result-stats', // Search statistics
'.yuRUbf', // Result link container
];
for (const indicator of resultIndicators) {
const element = document.querySelector(indicator);
if (element && element.children.length > 0) {
return true;
}
}
return false;
}
/**
* Extract search results from the current page with intelligent selector discovery
* @param {number} maxResults - Maximum number of results to extract
* @returns {Promise<Object>} - Extracted results
*/
async function extractSearchResults(maxResults = 10) {
try {
console.log('🔍 Starting intelligent search result extraction...');
const results = [];
// Try multiple selectors for Google search results
const resultSelectors = [
'.tF2Cxc', // Current Google search result container
'.g', // Traditional Google search result
'#rso .g', // Results container with .g class
'.yuRUbf', // Google result link container
'.rc', // Another Google result class
];
let resultElements = [];
let successfulSelector = null;
// First try standard selectors
for (const selector of resultSelectors) {
resultElements = document.querySelectorAll(selector);
if (resultElements.length > 0) {
successfulSelector = selector;
console.log(`✅ Found results with standard selector: ${selector}`);
break;
}
}
// If standard selectors fail, try intelligent discovery
if (resultElements.length === 0) {
console.log('🧠 Standard selectors failed, trying intelligent discovery...');
const discoveryResult = await discoverSearchResultElements();
resultElements = discoveryResult.elements;
successfulSelector = discoveryResult.selector;
}
// Extract results from found elements
for (let i = 0; i < Math.min(resultElements.length, maxResults); i++) {
const element = resultElements[i];
try {
const extractedResult = extractResultFromElement(element, i + 1);
if (extractedResult) {
results.push(extractedResult);
}
} catch (e) {
console.debug(`Error extracting result ${i}:`, e);
continue;
}
}
return {
success: true,
results,
totalFound: results.length,
selectorUsed: successfulSelector,
method: resultElements.length > 0 ? 'extraction' : 'none',
};
} catch (error) {
console.error('Error extracting search results:', error);
return {
success: false,
error: error.message,
results: [],
};
}
}
/**
* Intelligent discovery of search result elements
* @returns {Object} - Object with elements array and successful selector
*/
async function discoverSearchResultElements() {
console.log('🔬 Starting intelligent element discovery...');
// Intelligent selectors based on common patterns
const intelligentSelectors = [
// Modern Google patterns (2024+)
'[data-ved] h3',
'[data-ved]:has(h3)',
'[data-ved]:has(a[href*="http"])',
'[jscontroller]:has(h3)',
'[jscontroller]:has(a[href*="http"])',
// Generic search result patterns
'div[class*="result"]:has(h3)',
'div[class*="search"]:has(h3)',
'article:has(h3)',
'li[class*="result"]:has(h3)',
'[role="main"] div:has(h3)',
// Link-based patterns
'a[href*="http"]:has(h3)',
'div:has(h3):has(a[href*="http"])',
// Container patterns
'div[class*="container"] > div:has(h3)',
'div[id*="result"]:has(h3)',
'div[id*="search"]:has(h3)',
// Semantic patterns
'[role="article"]:has(h3)',
'[role="listitem"]:has(h3)',
'div[aria-label*="result"]:has(h3)',
// Fallback broad patterns
'main div:has(h3)',
'#main div:has(h3)',
'.main div:has(h3)',
'h3:has(+ div)',
'div:has(h3)',
];
for (const selector of intelligentSelectors) {
try {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
// Validate that these look like search results
const validElements = Array.from(elements).filter((el) =>
validateSearchResultElement(el),
);
if (validElements.length > 0) {
console.log(
`✅ Found ${validElements.length} results with intelligent selector: ${selector}`,
);
return {
elements: validElements,
selector: `intelligent-${selector}`,
};
}
}
} catch (e) {
console.debug(`Intelligent selector failed: ${selector}`, e);
continue;
}
}
// Final fallback - DOM structure analysis
console.log('🔬 Trying DOM structure analysis...');
return analyzeDOMForSearchResults();
}
/**
* Validate that an element looks like a search result
* @param {Element} element - Element to validate
* @returns {boolean} - True if element looks like a search result
*/
function validateSearchResultElement(element) {
try {
// Check for common search result indicators
const hasHeading = element.querySelector('h1, h2, h3, h4, h5, h6');
const hasLink = element.querySelector('a[href*="http"]');
const hasText = element.textContent && element.textContent.trim().length > 50;
// Must have at least heading and link, or substantial text
return (hasHeading && hasLink) || hasText;
} catch (e) {
return false;
}
}
/**
* Analyze DOM structure to find search results using heuristics
* @returns {Object} - Object with elements array and successful selector
*/
function analyzeDOMForSearchResults() {
console.log('🔬 Analyzing DOM structure for search results...');
try {
// Look for containers with multiple links (likely search results)
const heuristicSelectors = [
'div:has(a[href*="http"]):has(h3)',
'li:has(a[href*="http"]):has(h3)',
'article:has(a[href*="http"])',
'main > div:has(h3)',
'#main > div:has(h3)',
'[role="main"] > div:has(h3)',
'div:has(h3):has(a[href*="http"])',
'section:has(h3):has(a[href*="http"])',
];
for (const selector of heuristicSelectors) {
try {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
const validElements = Array.from(elements).filter((el) =>
validateSearchResultElement(el),
);
if (validElements.length > 0) {
console.log(
`✅ Found ${validElements.length} results with DOM analysis: ${selector}`,
);
return {
elements: validElements,
selector: `dom-analysis-${selector}`,
};
}
}
} catch (e) {
console.debug(`DOM analysis selector failed: ${selector}`, e);
continue;
}
}
// Ultimate fallback - any elements with links
const fallbackElements = document.querySelectorAll('a[href*="http"]');
if (fallbackElements.length > 0) {
console.log(`⚠️ Using fallback: found ${fallbackElements.length} link elements`);
return {
elements: Array.from(fallbackElements).slice(0, 10), // Limit to 10
selector: 'fallback-links',
};
}
console.warn('❌ DOM analysis failed to find any search results');
return {
elements: [],
selector: null,
};
} catch (e) {
console.error('Error in DOM analysis:', e);
return {
elements: [],
selector: null,
};
}
}
/**
* Extract result data from a single element
* @param {Element} element - Element to extract from
* @param {number} index - Result index
* @returns {Object|null} - Extracted result or null
*/
function extractResultFromElement(element, index) {
try {
// Try multiple patterns for title extraction
const titleSelectors = ['h3', 'h2', 'h1', '.LC20lb', '.DKV0Md', 'a[href*="http"]'];
let titleElement = null;
for (const selector of titleSelectors) {
titleElement = element.querySelector(selector);
if (titleElement) break;
}
// Try multiple patterns for link extraction
const linkElement =
element.querySelector('a[href*="http"]') || (element.tagName === 'A' ? element : null);
// Try multiple patterns for snippet extraction
const snippetSelectors = ['.VwiC3b', '.s', '.st', 'p', 'div:not(:has(h1,h2,h3,h4,h5,h6))'];
let snippetElement = null;
for (const selector of snippetSelectors) {
snippetElement = element.querySelector(selector);
if (snippetElement && snippetElement.textContent.trim().length > 20) break;
}
// Extract data
const title = titleElement?.textContent?.trim() || 'No title found';
const url = linkElement?.href || '';
const snippet = snippetElement?.textContent?.trim() || '';
// Validate we have meaningful data
if (title && title !== 'No title found' && url) {
return {
title,
url,
snippet,
index,
};
}
return null;
} catch (e) {
console.debug(`Error extracting from element:`, e);
return null;
}
}
/**
* Sleep utility function
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'performGoogleSearch') {
performGoogleSearch(request.selector, request.query)
.then(sendResponse)
.catch((error) => {
sendResponse({
success: false,
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
} else if (request.action === 'extractSearchResults') {
extractSearchResults(request.maxResults)
.then(sendResponse)
.catch((error) => {
sendResponse({
success: false,
error: `Unexpected error: ${error.message}`,
results: [],
});
});
return true; // Indicates async response
} else if (request.action === 'enhanced_search_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
}

View File

@@ -0,0 +1,277 @@
/* eslint-disable */
// form-submit-helper.js
// Enhanced form submission with multiple methods
if (window.__FORM_SUBMIT_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__FORM_SUBMIT_HELPER_INITIALIZED__ = true;
/**
* Submit a form using multiple methods
* @param {string} formSelector - CSS selector for the form
* @param {string} inputSelector - CSS selector for input field to focus (optional)
* @param {string} submitMethod - Preferred submission method
* @returns {Promise<Object>} - Result of the submission
*/
async function submitForm(formSelector = 'form', inputSelector = null, submitMethod = 'auto') {
try {
console.log(`🔄 Attempting form submission with method: ${submitMethod}`);
// Find the form
let form = null;
if (formSelector) {
form = document.querySelector(formSelector);
}
// If no specific form found, try to find the form containing the input
if (!form && inputSelector) {
const input = document.querySelector(inputSelector);
if (input) {
form = input.closest('form');
}
}
// If still no form, try to find any form on the page
if (!form) {
form = document.querySelector('form');
}
if (!form) {
return {
success: false,
error: 'No form found on the page',
};
}
// Focus input if specified
if (inputSelector) {
const input = document.querySelector(inputSelector);
if (input) {
input.focus();
await sleep(200);
}
}
// Try submission based on method
let result = null;
if (submitMethod === 'enter' || submitMethod === 'auto') {
result = await tryEnterKeySubmission(form, inputSelector);
if (result && result.success) {
return result;
}
}
if (submitMethod === 'button' || submitMethod === 'auto') {
result = await tryButtonSubmission(form);
if (result && result.success) {
return result;
}
}
if (submitMethod === 'auto') {
result = await tryFormSubmission(form);
if (result && result.success) {
return result;
}
}
return {
success: false,
error: 'All submission methods failed',
attemptedMethods: submitMethod === 'auto' ? ['enter', 'button', 'form'] : [submitMethod],
};
} catch (error) {
console.error('Error in submitForm:', error);
return {
success: false,
error: `Unexpected error: ${error.message}`,
};
}
}
/**
* Try submitting form using Enter key
* @param {Element} form - The form element
* @param {string} inputSelector - Input selector to focus
* @returns {Promise<Object|null>}
*/
async function tryEnterKeySubmission(form, inputSelector) {
try {
console.log('🔄 Trying Enter key submission');
let targetElement = null;
if (inputSelector) {
targetElement = document.querySelector(inputSelector);
}
if (!targetElement) {
// Find the first input in the form
targetElement = form.querySelector('input[type="text"], input[type="search"], textarea, input:not([type])');
}
if (!targetElement) {
return null;
}
targetElement.focus();
await sleep(200);
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
targetElement.dispatchEvent(enterEvent);
// Also try keypress and keyup for compatibility
const enterPress = new KeyboardEvent('keypress', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
targetElement.dispatchEvent(enterPress);
const enterUp = new KeyboardEvent('keyup', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
targetElement.dispatchEvent(enterUp);
await sleep(500);
return {
success: true,
method: 'enter_key',
element: targetElement.tagName.toLowerCase(),
};
} catch (error) {
console.debug('Enter key submission failed:', error);
return null;
}
}
/**
* Try submitting form by clicking submit button
* @param {Element} form - The form element
* @returns {Promise<Object|null>}
*/
async function tryButtonSubmission(form) {
try {
console.log('🔄 Trying button submission');
const buttonSelectors = [
'input[type="submit"]',
'button[type="submit"]',
'button:not([type])', // Default button type is submit
'input[value*="Search" i]',
'input[value*="Submit" i]',
'input[value*="Send" i]',
'button:contains("Search")',
'button:contains("Submit")',
'button:contains("Send")',
'.submit-btn',
'.search-btn',
'.btn-submit',
'[role="button"][aria-label*="search" i]',
'[role="button"][aria-label*="submit" i]'
];
for (const selector of buttonSelectors) {
try {
let button = form.querySelector(selector);
// If not found in form, try the whole document
if (!button) {
button = document.querySelector(selector);
}
if (button) {
button.click();
await sleep(300);
return {
success: true,
method: 'button_click',
selector: selector,
element: button.tagName.toLowerCase(),
};
}
} catch (e) {
continue;
}
}
return null;
} catch (error) {
console.debug('Button submission failed:', error);
return null;
}
}
/**
* Try submitting form using form.submit()
* @param {Element} form - The form element
* @returns {Promise<Object|null>}
*/
async function tryFormSubmission(form) {
try {
console.log('🔄 Trying form.submit()');
form.submit();
await sleep(300);
return {
success: true,
method: 'form_submit',
};
} catch (error) {
console.debug('Form submission failed:', error);
return null;
}
}
/**
* Sleep utility function
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'submitForm') {
submitForm(request.formSelector, request.inputSelector, request.submitMethod)
.then(sendResponse)
.catch((error) => {
sendResponse({
success: false,
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
} else if (request.action === 'form_submit_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
}

View File

@@ -0,0 +1,147 @@
/**
* Chrome Extension User ID Helper
* This script provides easy access to the Chrome extension user ID in any web page
*/
(function() {
'use strict';
// Namespace for Chrome extension user ID functionality
window.ChromeExtensionUserID = {
// Current user ID (will be populated when available)
userId: null,
// Callbacks to execute when user ID becomes available
callbacks: [],
/**
* Get the current user ID
* @returns {Promise<string|null>} The user ID or null if not available
*/
async getUserId() {
// If already available, return immediately
if (this.userId) {
return this.userId;
}
// Try to get from sessionStorage first
try {
const storedUserId = sessionStorage.getItem('chromeExtensionUserId');
if (storedUserId) {
this.userId = storedUserId;
return storedUserId;
}
} catch (e) {
// Ignore storage errors
}
// Try to get from global window variable
if (window.chromeExtensionUserId) {
this.userId = window.chromeExtensionUserId;
return this.userId;
}
// Request from content script
return new Promise((resolve) => {
// Set up listener for the custom event
const listener = (event) => {
if (event.detail && event.detail.userId) {
this.userId = event.detail.userId;
window.removeEventListener('chromeExtensionUserIdReady', listener);
resolve(this.userId);
}
};
window.addEventListener('chromeExtensionUserIdReady', listener);
// Also check if it's already available
setTimeout(() => {
if (window.chromeExtensionUserId) {
this.userId = window.chromeExtensionUserId;
window.removeEventListener('chromeExtensionUserIdReady', listener);
resolve(this.userId);
} else {
// Timeout after 5 seconds
setTimeout(() => {
window.removeEventListener('chromeExtensionUserIdReady', listener);
resolve(null);
}, 5000);
}
}, 100);
});
},
/**
* Execute callback when user ID becomes available
* @param {Function} callback - Function to execute with user ID
*/
onUserIdReady(callback) {
if (this.userId) {
// Execute immediately if already available
callback(this.userId);
} else {
// Store callback for later execution
this.callbacks.push(callback);
// Try to get user ID
this.getUserId().then((userId) => {
if (userId) {
// Execute all pending callbacks
this.callbacks.forEach(cb => cb(userId));
this.callbacks = [];
}
});
}
},
/**
* Check if user ID is available
* @returns {boolean} True if user ID is available
*/
isAvailable() {
return this.userId !== null;
},
/**
* Get user ID synchronously (only if already loaded)
* @returns {string|null} The user ID or null if not loaded
*/
getUserIdSync() {
return this.userId || window.chromeExtensionUserId || null;
}
};
// Auto-initialize when script loads
window.ChromeExtensionUserID.getUserId().then((userId) => {
if (userId) {
console.log('Chrome Extension User ID Helper: User ID loaded:', userId);
} else {
console.log('Chrome Extension User ID Helper: No user ID available');
}
});
// Listen for the custom event in case it comes later
window.addEventListener('chromeExtensionUserIdReady', (event) => {
if (event.detail && event.detail.userId) {
window.ChromeExtensionUserID.userId = event.detail.userId;
console.log('Chrome Extension User ID Helper: User ID received via event:', event.detail.userId);
// Execute any pending callbacks
window.ChromeExtensionUserID.callbacks.forEach(callback => callback(event.detail.userId));
window.ChromeExtensionUserID.callbacks = [];
}
});
})();
// Also provide a simple global function for easy access
window.getChromeExtensionUserId = function() {
return window.ChromeExtensionUserID.getUserId();
};
// Provide a synchronous version
window.getChromeExtensionUserIdSync = function() {
return window.ChromeExtensionUserID.getUserIdSync();
};
console.log('Chrome Extension User ID Helper loaded. Use window.getChromeExtensionUserId() or window.ChromeExtensionUserID.getUserId()');

View File

@@ -8,7 +8,8 @@
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build": "wxt build && node -e \"const fs = require('fs'); const path = require('path'); const src = path.join('.output', 'chrome-mv3', '_locales', 'en', 'messages.json'); const dest = path.join('.output', 'chrome-mv3', '_locales', 'messages.json'); if (fs.existsSync(src)) { fs.copyFileSync(src, dest); console.log('Copied default locale file'); }\"",
"build:basic": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",

View File

@@ -40,6 +40,7 @@ const fallbackMessages: Record<string, string> = {
connectionPortLabel: 'Connection Port',
refreshStatusButton: 'Refresh Status',
copyConfigButton: 'Copy Configuration',
copyUserIdButton: 'Copy User ID',
// Action buttons
retryButton: 'Retry',

File diff suppressed because it is too large Load Diff

View File

@@ -28,21 +28,19 @@ export default defineConfig({
manifest: {
// Use environment variable for the key, fallback to undefined if not set
key: CHROME_EXTENSION_KEY,
default_locale: 'zh_CN',
default_locale: 'en',
name: '__MSG_extensionName__',
description: '__MSG_extensionDescription__',
permissions: [
'nativeMessaging',
'tabs',
'activeTab',
'storage',
'scripting',
'downloads',
'webRequest',
'debugger',
'webRequest',
'history',
'bookmarks',
'offscreen',
'storage',
],
host_permissions: ['<all_urls>'],
web_accessible_resources: [