Major refactor: Multi-user Chrome MCP extension with remote server architecture
This commit is contained in:
@@ -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
|
||||
|
133
app/chrome-extension/PERSISTENT_CONNECTION_CHANGES.md
Normal file
133
app/chrome-extension/PERSISTENT_CONNECTION_CHANGES.md
Normal 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
|
170
app/chrome-extension/PERSISTENT_CONNECTION_TEST_PLAN.md
Normal file
170
app/chrome-extension/PERSISTENT_CONNECTION_TEST_PLAN.md
Normal 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
|
178
app/chrome-extension/USER_ID_GUIDE.md
Normal file
178
app/chrome-extension/USER_ID_GUIDE.md
Normal 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}`);
|
||||
}
|
||||
```
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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": "远程服务器错误状态"
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
35
app/chrome-extension/common/env-config.ts
Normal file
35
app/chrome-extension/common/env-config.ts
Normal 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;
|
@@ -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,
|
||||
};
|
||||
};
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
}
|
||||
})();
|
@@ -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';
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
);
|
||||
|
@@ -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
@@ -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;
|
||||
}
|
||||
|
560
app/chrome-extension/inject-scripts/enhanced-search-helper.js
Normal file
560
app/chrome-extension/inject-scripts/enhanced-search-helper.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
277
app/chrome-extension/inject-scripts/form-submit-helper.js
Normal file
277
app/chrome-extension/inject-scripts/form-submit-helper.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
147
app/chrome-extension/inject-scripts/user-id-helper.js
Normal file
147
app/chrome-extension/inject-scripts/user-id-helper.js
Normal 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()');
|
@@ -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",
|
||||
|
@@ -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',
|
||||
|
1283
app/chrome-extension/utils/remote-server-client.ts
Normal file
1283
app/chrome-extension/utils/remote-server-client.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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: [
|
||||
|
Reference in New Issue
Block a user