mirror of
https://github.com/aaronjmars/opendia.git
synced 2025-12-29 16:16:00 +00:00
better tools
This commit is contained in:
@@ -8,15 +8,20 @@ let reconnectAttempts = 0;
|
||||
function connectToMCPServer() {
|
||||
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return;
|
||||
|
||||
console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL);
|
||||
mcpSocket = new WebSocket(MCP_SERVER_URL);
|
||||
|
||||
mcpSocket.onopen = () => {
|
||||
console.log('✅ Connected to MCP server');
|
||||
clearInterval(reconnectInterval);
|
||||
|
||||
const tools = getAvailableTools();
|
||||
console.log(`🔧 Registering ${tools.length} tools:`, tools.map(t => t.name));
|
||||
|
||||
// Register available browser functions
|
||||
mcpSocket.send(JSON.stringify({
|
||||
type: 'register',
|
||||
tools: getAvailableTools()
|
||||
tools: tools
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -26,21 +31,193 @@ function connectToMCPServer() {
|
||||
};
|
||||
|
||||
mcpSocket.onclose = () => {
|
||||
console.log('❌ Disconnected from MCP server, will reconnect...');
|
||||
// Attempt to reconnect every 5 seconds
|
||||
reconnectInterval = setInterval(connectToMCPServer, 5000);
|
||||
};
|
||||
|
||||
mcpSocket.onerror = (error) => {
|
||||
// Handle error silently in production
|
||||
console.log('⚠️ MCP WebSocket error:', error);
|
||||
};
|
||||
}
|
||||
|
||||
// Define available browser tools for MCP
|
||||
// Define available browser automation tools for MCP
|
||||
function getAvailableTools() {
|
||||
return [
|
||||
// Page Analysis Tools
|
||||
{
|
||||
name: "page_analyze",
|
||||
description: "Two-phase intelligent page analysis with token efficiency optimization",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
intent_hint: {
|
||||
type: "string",
|
||||
description: "User intent: login, signup, search, post_create, comment, menu, submit, etc."
|
||||
},
|
||||
phase: {
|
||||
type: "string",
|
||||
enum: ["discover", "detailed"],
|
||||
default: "discover",
|
||||
description: "Analysis phase: 'discover' for quick scan (<100 tokens), 'detailed' for full analysis"
|
||||
},
|
||||
focus_areas: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Areas to analyze in detail: buttons, forms, navigation, search_elements"
|
||||
},
|
||||
max_results: {
|
||||
type: "number",
|
||||
default: 5,
|
||||
maximum: 15,
|
||||
description: "Maximum number of elements to return"
|
||||
},
|
||||
element_ids: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Expand specific quick match IDs from discover phase (e.g. ['q1', 'q2'])"
|
||||
}
|
||||
},
|
||||
required: ["intent_hint"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "page_extract_content",
|
||||
description: "Extract and summarize structured content with token efficiency optimization",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
content_type: {
|
||||
type: "string",
|
||||
enum: ["article", "search_results", "posts"],
|
||||
description: "Type of content to extract"
|
||||
},
|
||||
max_items: {
|
||||
type: "number",
|
||||
description: "Maximum number of items to extract (for lists/collections)",
|
||||
default: 20
|
||||
},
|
||||
summarize: {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "Return summary instead of full content to save tokens"
|
||||
}
|
||||
},
|
||||
required: ["content_type"]
|
||||
}
|
||||
},
|
||||
|
||||
// Element Interaction Tools
|
||||
{
|
||||
name: "element_click",
|
||||
description: "Click on a specific page element",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
element_id: {
|
||||
type: "string",
|
||||
description: "Unique element identifier from page_analyze"
|
||||
},
|
||||
click_type: {
|
||||
type: "string",
|
||||
enum: ["left", "right", "double"],
|
||||
default: "left"
|
||||
},
|
||||
wait_after: {
|
||||
type: "number",
|
||||
description: "Milliseconds to wait after click",
|
||||
default: 500
|
||||
}
|
||||
},
|
||||
required: ["element_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "element_fill",
|
||||
description: "Fill input field with enhanced focus and event simulation for modern web apps",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
element_id: {
|
||||
type: "string",
|
||||
description: "Unique element identifier from page_analyze"
|
||||
},
|
||||
value: {
|
||||
type: "string",
|
||||
description: "Text value to input"
|
||||
},
|
||||
clear_first: {
|
||||
type: "boolean",
|
||||
description: "Clear existing content before filling",
|
||||
default: true
|
||||
},
|
||||
force_focus: {
|
||||
type: "boolean",
|
||||
description: "Use enhanced focus sequence with click simulation for modern apps",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
required: ["element_id", "value"]
|
||||
}
|
||||
},
|
||||
|
||||
// Navigation Tools
|
||||
{
|
||||
name: "page_navigate",
|
||||
description: "Navigate to specified URL and wait for page load",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: "URL to navigate to"
|
||||
},
|
||||
wait_for: {
|
||||
type: "string",
|
||||
description: "CSS selector to wait for after navigation (ensures page is ready)"
|
||||
},
|
||||
timeout: {
|
||||
type: "number",
|
||||
description: "Maximum wait time in milliseconds",
|
||||
default: 10000
|
||||
}
|
||||
},
|
||||
required: ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "page_wait_for",
|
||||
description: "Wait for specific element or condition on current page",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
condition_type: {
|
||||
type: "string",
|
||||
enum: ["element_visible", "text_present"],
|
||||
description: "Type of condition to wait for"
|
||||
},
|
||||
selector: {
|
||||
type: "string",
|
||||
description: "CSS selector for element-based conditions"
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
description: "Text to wait for (when condition_type is 'text_present')"
|
||||
},
|
||||
timeout: {
|
||||
type: "number",
|
||||
description: "Maximum wait time in milliseconds",
|
||||
default: 5000
|
||||
}
|
||||
},
|
||||
required: ["condition_type"]
|
||||
}
|
||||
},
|
||||
|
||||
// Essential legacy tools for compatibility
|
||||
{
|
||||
name: "browser_navigate",
|
||||
description: "Navigate to a URL in the active tab",
|
||||
description: "Navigate to a URL in the active tab (legacy)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -49,39 +226,6 @@ function getAvailableTools() {
|
||||
required: ["url"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_get_tabs",
|
||||
description: "Get all open tabs",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_create_tab",
|
||||
description: "Create a new tab",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string", description: "URL for the new tab" },
|
||||
active: {
|
||||
type: "boolean",
|
||||
description: "Whether to make the tab active",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_close_tab",
|
||||
description: "Close a tab by ID",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: { type: "number", description: "ID of the tab to close" },
|
||||
},
|
||||
required: ["tabId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_execute_script",
|
||||
description: "Execute JavaScript in the active tab",
|
||||
@@ -93,111 +237,46 @@ function getAvailableTools() {
|
||||
required: ["code"],
|
||||
},
|
||||
},
|
||||
|
||||
// Element State Tools
|
||||
{
|
||||
name: "browser_get_page_content",
|
||||
description: "Get the content of the active page",
|
||||
name: "element_get_state",
|
||||
description: "Get detailed state information for a specific element (disabled, clickable, etc.)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: {
|
||||
element_id: {
|
||||
type: "string",
|
||||
description: "CSS selector to get specific content",
|
||||
},
|
||||
description: "Element ID from page_analyze"
|
||||
}
|
||||
},
|
||||
},
|
||||
required: ["element_id"]
|
||||
}
|
||||
},
|
||||
|
||||
// Analytics and Performance Tools
|
||||
{
|
||||
name: "browser_take_screenshot",
|
||||
description: "Take a screenshot of the active tab",
|
||||
name: "get_analytics",
|
||||
description: "Get token usage analytics and performance metrics",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
format: {
|
||||
type: "string",
|
||||
enum: ["png", "jpeg"],
|
||||
description: "Image format",
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {},
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "browser_get_bookmarks",
|
||||
description: "Get browser bookmarks",
|
||||
name: "clear_analytics",
|
||||
description: "Clear all analytics data and reset performance tracking",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query for bookmarks" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_add_bookmark",
|
||||
description: "Add a bookmark",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "Bookmark title" },
|
||||
url: { type: "string", description: "Bookmark URL" },
|
||||
},
|
||||
required: ["title", "url"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_get_history",
|
||||
description: "Search browser history",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query" },
|
||||
maxResults: {
|
||||
type: "number",
|
||||
description: "Maximum number of results",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_get_cookies",
|
||||
description: "Get cookies for a domain",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
domain: { type: "string", description: "Domain to get cookies for" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_fill_form",
|
||||
description: "Fill a form on the current page",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
formData: {
|
||||
type: "object",
|
||||
description: "Key-value pairs of form field names/IDs and values",
|
||||
},
|
||||
},
|
||||
required: ["formData"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_click_element",
|
||||
description: "Click an element on the page",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: {
|
||||
type: "string",
|
||||
description: "CSS selector of element to click",
|
||||
},
|
||||
},
|
||||
required: ["selector"],
|
||||
},
|
||||
properties: {},
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Handle MCP requests
|
||||
// Handle MCP requests with enhanced automation tools
|
||||
async function handleMCPRequest(message) {
|
||||
const { id, method, params } = message;
|
||||
|
||||
@@ -205,44 +284,45 @@ async function handleMCPRequest(message) {
|
||||
let result;
|
||||
|
||||
switch (method) {
|
||||
// New automation tools
|
||||
case "page_analyze":
|
||||
result = await sendToContentScript('analyze', params);
|
||||
break;
|
||||
case "page_extract_content":
|
||||
result = await sendToContentScript('extract_content', params);
|
||||
break;
|
||||
case "element_click":
|
||||
result = await sendToContentScript('element_click', params);
|
||||
break;
|
||||
case "element_fill":
|
||||
result = await sendToContentScript('element_fill', params);
|
||||
break;
|
||||
case "page_navigate":
|
||||
result = await navigateToUrl(params.url, params.wait_for, params.timeout);
|
||||
break;
|
||||
case "page_wait_for":
|
||||
result = await sendToContentScript('wait_for', params);
|
||||
break;
|
||||
|
||||
// Essential legacy tools for compatibility
|
||||
case "browser_navigate":
|
||||
result = await navigateToUrl(params.url);
|
||||
break;
|
||||
case "browser_get_tabs":
|
||||
result = await getTabs();
|
||||
break;
|
||||
case "browser_create_tab":
|
||||
result = await createTab(params);
|
||||
break;
|
||||
case "browser_close_tab":
|
||||
result = await closeTab(params.tabId);
|
||||
break;
|
||||
case "browser_execute_script":
|
||||
result = await executeScript(params.code);
|
||||
break;
|
||||
case "browser_get_page_content":
|
||||
result = await getPageContent(params.selector);
|
||||
|
||||
// Element state tools
|
||||
case "element_get_state":
|
||||
result = await sendToContentScript('get_element_state', params);
|
||||
break;
|
||||
case "browser_take_screenshot":
|
||||
result = await takeScreenshot(params.format);
|
||||
|
||||
// Analytics tools
|
||||
case "get_analytics":
|
||||
result = await sendToContentScript('get_analytics', {});
|
||||
break;
|
||||
case "browser_get_bookmarks":
|
||||
result = await getBookmarks(params.query);
|
||||
break;
|
||||
case "browser_add_bookmark":
|
||||
result = await addBookmark(params);
|
||||
break;
|
||||
case "browser_get_history":
|
||||
result = await getHistory(params);
|
||||
break;
|
||||
case "browser_get_cookies":
|
||||
result = await getCookies(params.domain);
|
||||
break;
|
||||
case "browser_fill_form":
|
||||
result = await fillForm(params.formData);
|
||||
break;
|
||||
case "browser_click_element":
|
||||
result = await clickElement(params.selector);
|
||||
case "clear_analytics":
|
||||
result = await sendToContentScript('clear_analytics', {});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
@@ -269,38 +349,76 @@ async function handleMCPRequest(message) {
|
||||
}
|
||||
}
|
||||
|
||||
// Browser function implementations
|
||||
async function navigateToUrl(url) {
|
||||
// Enhanced content script communication
|
||||
async function sendToContentScript(action, data) {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
await chrome.tabs.update(activeTab.id, { url });
|
||||
return { success: true, tabId: activeTab.id };
|
||||
}
|
||||
|
||||
async function getTabs() {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
return tabs.map((tab) => ({
|
||||
id: tab.id,
|
||||
title: tab.title,
|
||||
url: tab.url,
|
||||
active: tab.active,
|
||||
windowId: tab.windowId,
|
||||
}));
|
||||
}
|
||||
|
||||
async function createTab(params) {
|
||||
const tab = await chrome.tabs.create({
|
||||
url: params.url || "about:blank",
|
||||
active: params.active !== false,
|
||||
|
||||
if (!activeTab) {
|
||||
throw new Error('No active tab found');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(activeTab.id, { action, data }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
} else if (response.success) {
|
||||
resolve(response.data);
|
||||
} else {
|
||||
reject(new Error(response.error || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
});
|
||||
return { id: tab.id, windowId: tab.windowId };
|
||||
}
|
||||
|
||||
async function closeTab(tabId) {
|
||||
await chrome.tabs.remove(tabId);
|
||||
return { success: true };
|
||||
async function navigateToUrl(url, waitFor, timeout = 10000) {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
await chrome.tabs.update(activeTab.id, { url });
|
||||
|
||||
// If waitFor is specified, wait for the element to appear
|
||||
if (waitFor) {
|
||||
try {
|
||||
await waitForElement(activeTab.id, waitFor, timeout);
|
||||
} catch (error) {
|
||||
return { success: true, tabId: activeTab.id, warning: `Navigation completed but wait condition failed: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, tabId: activeTab.id, url: url };
|
||||
}
|
||||
|
||||
async function waitForElement(tabId, selector, timeout = 5000) {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
const result = await chrome.tabs.sendMessage(tabId, {
|
||||
action: 'wait_for',
|
||||
data: {
|
||||
condition_type: 'element_visible',
|
||||
selector: selector,
|
||||
timeout: 1000
|
||||
}
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Content script might not be ready yet, continue waiting
|
||||
}
|
||||
|
||||
// Wait 500ms before next check
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
throw new Error(`Timeout waiting for element: ${selector}`);
|
||||
}
|
||||
|
||||
async function executeScript(code) {
|
||||
@@ -308,105 +426,55 @@ async function executeScript(code) {
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: activeTab.id },
|
||||
func: new Function(code),
|
||||
});
|
||||
return results[0].result;
|
||||
}
|
||||
|
||||
async function getPageContent(selector) {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: activeTab.id },
|
||||
func: (sel) => {
|
||||
if (sel) {
|
||||
const element = document.querySelector(sel);
|
||||
return element ? element.innerText : null;
|
||||
}
|
||||
return document.body.innerText;
|
||||
},
|
||||
args: [selector],
|
||||
});
|
||||
return results[0].result;
|
||||
}
|
||||
|
||||
async function takeScreenshot(format = "png") {
|
||||
const dataUrl = await chrome.tabs.captureVisibleTab(null, { format });
|
||||
return { dataUrl, format };
|
||||
}
|
||||
|
||||
async function getBookmarks(query) {
|
||||
if (query) {
|
||||
return await chrome.bookmarks.search(query);
|
||||
}
|
||||
return await chrome.bookmarks.getTree();
|
||||
}
|
||||
|
||||
async function addBookmark(params) {
|
||||
const bookmark = await chrome.bookmarks.create({
|
||||
title: params.title,
|
||||
url: params.url,
|
||||
});
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
async function getHistory(params) {
|
||||
const historyItems = await chrome.history.search({
|
||||
text: params.query || "",
|
||||
maxResults: params.maxResults || 100,
|
||||
});
|
||||
return historyItems;
|
||||
}
|
||||
|
||||
async function getCookies(domain) {
|
||||
const cookies = await chrome.cookies.getAll({ domain });
|
||||
return cookies;
|
||||
}
|
||||
|
||||
async function fillForm(formData) {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: activeTab.id },
|
||||
func: (data) => {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const element = document.querySelector(`[name="${key}"], #${key}`);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
try {
|
||||
// Use chrome.scripting.executeScript with a function instead of eval
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: activeTab.id },
|
||||
func: (codeToExecute) => {
|
||||
// Execute code safely without eval
|
||||
try {
|
||||
// Create a script element to bypass CSP
|
||||
const script = document.createElement('script');
|
||||
script.textContent = codeToExecute;
|
||||
document.head.appendChild(script);
|
||||
document.head.removeChild(script);
|
||||
return { success: true, executed: true };
|
||||
} catch (error) {
|
||||
// Fallback: try to execute common operations directly
|
||||
if (codeToExecute.includes('window.scrollTo')) {
|
||||
const match = codeToExecute.match(/window\.scrollTo\((\d+),\s*(\d+|[^)]+)\)/);
|
||||
if (match) {
|
||||
const x = parseInt(match[1]);
|
||||
const y = match[2].includes('document.body.scrollHeight') ?
|
||||
document.body.scrollHeight / 2 : parseInt(match[2]);
|
||||
window.scrollTo(x, y);
|
||||
return { success: true, scrolled: true, x, y };
|
||||
}
|
||||
}
|
||||
|
||||
if (codeToExecute.includes('document.querySelector')) {
|
||||
// Handle simple querySelector operations
|
||||
const match = codeToExecute.match(/document\.querySelector\(['"]([^'"]+)['"]\)/);
|
||||
if (match) {
|
||||
const element = document.querySelector(match[1]);
|
||||
return { success: true, element: element ? 'found' : 'not found', selector: match[1] };
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return { success: true, filled: Object.keys(data).length };
|
||||
},
|
||||
args: [formData],
|
||||
});
|
||||
return results[0].result;
|
||||
}
|
||||
|
||||
async function clickElement(selector) {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: activeTab.id },
|
||||
func: (sel) => {
|
||||
const element = document.querySelector(sel);
|
||||
if (element) {
|
||||
element.click();
|
||||
return { success: true, clicked: sel };
|
||||
}
|
||||
throw new Error(`Element not found: ${sel}`);
|
||||
},
|
||||
args: [selector],
|
||||
});
|
||||
return results[0].result;
|
||||
},
|
||||
args: [code],
|
||||
});
|
||||
return results[0].result;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
note: "CSP restrictions prevent arbitrary JavaScript execution. Try using specific automation tools instead."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize connection when extension loads
|
||||
@@ -435,4 +503,4 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
sendResponse({ success: true });
|
||||
}
|
||||
return true; // Keep the message channel open
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,15 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "OpenDia Browser Bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "Exposes browser functions through Model Context Protocol",
|
||||
"name": "OpenDia Enhanced Browser Automation",
|
||||
"version": "2.0.0",
|
||||
"description": "Enhanced browser automation through Model Context Protocol with pattern database and semantic analysis",
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"activeTab",
|
||||
"storage",
|
||||
"bookmarks",
|
||||
"history",
|
||||
"downloads",
|
||||
"cookies",
|
||||
"webNavigation",
|
||||
"scripting",
|
||||
"nativeMessaging",
|
||||
"contextMenus",
|
||||
"notifications",
|
||||
"alarms",
|
||||
"clipboardRead",
|
||||
"clipboardWrite"
|
||||
"webNavigation",
|
||||
"notifications"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
@@ -28,7 +19,7 @@
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "OpenDia Browser Bridge"
|
||||
"default_title": "OpenDia Enhanced Browser Automation"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
@@ -41,4 +32,4 @@
|
||||
"ids": ["*"],
|
||||
"matches": ["http://localhost/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OpenDia Browser Bridge</title>
|
||||
<style>
|
||||
:root {
|
||||
@@ -125,6 +126,167 @@
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
|
||||
.testing-section {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.test-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select, input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
background: white;
|
||||
color: var(--text-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
select:focus, input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.test-results {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.75rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.test-results-expanded {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.test-results.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
font-size: 0.7rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.7rem;
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e5e7eb;
|
||||
margin: 8px 0;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
background-color: #6b7280;
|
||||
font-size: 0.7rem;
|
||||
padding: 4px 8px;
|
||||
margin-top: 8px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expand-button:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.json-viewer {
|
||||
background: #1f2937;
|
||||
color: #f3f4f6;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.65rem;
|
||||
white-space: pre-wrap;
|
||||
margin: 8px 0;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #374151;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.size-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.size-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.size-value.large {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.size-value.normal {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.test-button {
|
||||
background-color: #10b981;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.test-button:hover {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.highlight-button {
|
||||
background-color: #f59e0b;
|
||||
font-size: 0.75rem;
|
||||
padding: 6px 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.highlight-button:hover {
|
||||
background-color: #d97706;
|
||||
}
|
||||
|
||||
.log {
|
||||
background-color: #f8fafc;
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -170,7 +332,11 @@
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Available Tools</span>
|
||||
<span class="info-value" id="toolCount">0</span>
|
||||
<span class="info-value" id="toolCount">8</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Current Page</span>
|
||||
<span class="info-value" id="currentPage">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -179,6 +345,40 @@
|
||||
<button id="testBtn">Test Connection</button>
|
||||
</div>
|
||||
|
||||
<div class="testing-section">
|
||||
<div class="section-title">🔬 Testing Tools</div>
|
||||
|
||||
<div class="test-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="content-type">Content Extraction</label>
|
||||
<select id="content-type">
|
||||
<option value="article">Article Content</option>
|
||||
<option value="search_results">Search Results</option>
|
||||
<option value="posts">Posts/Feed Items</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="test-button" id="test-extract">Test Extract</button>
|
||||
</div>
|
||||
|
||||
<div class="test-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="intent-hint">Element Analysis</label>
|
||||
<input type="text" id="intent-hint" placeholder="e.g., search, post_tweet, login">
|
||||
</div>
|
||||
<button class="test-button" id="test-analyze">Test Analyze</button>
|
||||
<button class="highlight-button" id="highlight-elements">🎯 Highlight</button>
|
||||
</div>
|
||||
|
||||
<div class="test-results" id="results">
|
||||
<div class="result-header">Test Results</div>
|
||||
<div class="result-meta" id="result-meta"></div>
|
||||
<div class="result-content" id="result-content"></div>
|
||||
<div class="size-info" id="data-size"></div>
|
||||
<button class="expand-button" id="expand-results">📄 View Full JSON</button>
|
||||
<div class="json-viewer" id="json-viewer" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log" id="log">
|
||||
<div class="log-entry">
|
||||
<span class="log-time">[System]</span>
|
||||
@@ -188,4 +388,4 @@
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,14 +1,21 @@
|
||||
// Popup script for status display
|
||||
// Enhanced Popup with Testing Interface
|
||||
let logContainer = document.getElementById("log");
|
||||
let statusIndicator = document.getElementById("statusIndicator");
|
||||
let statusText = document.getElementById("statusText");
|
||||
let toolCount = document.getElementById("toolCount");
|
||||
let currentPage = document.getElementById("currentPage");
|
||||
let resultArea = document.getElementById("results");
|
||||
let resultMeta = document.getElementById("result-meta");
|
||||
let resultContent = document.getElementById("result-content");
|
||||
let dataSizeInfo = document.getElementById("data-size");
|
||||
let expandButton = document.getElementById("expand-results");
|
||||
let jsonViewer = document.getElementById("json-viewer");
|
||||
|
||||
// Get initial tool count
|
||||
const tools = 13; // Number of tools we expose
|
||||
// Get initial tool count and page info
|
||||
const tools = 8; // 6 core automation + 2 essential legacy tools
|
||||
toolCount.textContent = tools;
|
||||
|
||||
// Check connection status
|
||||
// Check connection status and get page info
|
||||
function checkStatus() {
|
||||
if (chrome.runtime?.id) {
|
||||
chrome.runtime.sendMessage({ action: "getStatus" }, (response) => {
|
||||
@@ -19,6 +26,14 @@ function checkStatus() {
|
||||
updateStatus(response?.connected || false);
|
||||
}
|
||||
});
|
||||
|
||||
// Get current page info
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
if (tabs[0]) {
|
||||
const url = new URL(tabs[0].url);
|
||||
currentPage.textContent = url.hostname;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updateStatus(false);
|
||||
addLog("Extension context invalid", "error");
|
||||
@@ -67,6 +82,282 @@ document.getElementById("testBtn").addEventListener("click", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Testing Interface Functions
|
||||
class TestingInterface {
|
||||
constructor() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('test-extract').addEventListener('click', () => this.testExtraction());
|
||||
document.getElementById('test-analyze').addEventListener('click', () => this.testAnalysis());
|
||||
document.getElementById('highlight-elements').addEventListener('click', () => this.highlightElements());
|
||||
expandButton.addEventListener('click', () => this.toggleJsonViewer());
|
||||
}
|
||||
|
||||
async testExtraction() {
|
||||
const contentType = document.getElementById('content-type').value;
|
||||
addLog(`🔍 Testing content extraction: ${contentType}`, "info");
|
||||
|
||||
try {
|
||||
const result = await this.sendToContentScript({
|
||||
action: 'extract_content',
|
||||
data: { content_type: contentType }
|
||||
});
|
||||
|
||||
this.displayResults(result, 'Content Extraction');
|
||||
addLog(`Extraction completed in ${result.execution_time}ms`, "success");
|
||||
} catch (error) {
|
||||
addLog(`Extraction failed: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async testAnalysis() {
|
||||
const intentHint = document.getElementById('intent-hint').value;
|
||||
if (!intentHint.trim()) {
|
||||
addLog('Please enter an intent hint', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
addLog(`🎯 Testing page analysis: ${intentHint}`, "info");
|
||||
|
||||
try {
|
||||
const result = await this.sendToContentScript({
|
||||
action: 'analyze',
|
||||
data: { intent_hint: intentHint }
|
||||
});
|
||||
|
||||
this.displayResults(result, 'Page Analysis');
|
||||
addLog(`Analysis completed in ${result.execution_time}ms`, "success");
|
||||
} catch (error) {
|
||||
addLog(`Analysis failed: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async highlightElements() {
|
||||
const intentHint = document.getElementById('intent-hint').value;
|
||||
if (!intentHint.trim()) {
|
||||
addLog('Please enter an intent hint to highlight elements', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.sendToContentScript({
|
||||
action: 'analyze',
|
||||
data: { intent_hint: intentHint }
|
||||
});
|
||||
|
||||
if (result.success && result.data.elements?.length > 0) {
|
||||
// Inject highlighting script
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId: tabs[0].id },
|
||||
func: this.highlightElementsOnPage,
|
||||
args: [result.data.elements]
|
||||
});
|
||||
});
|
||||
|
||||
addLog(`🎯 Highlighted ${result.data.elements.length} elements`, "success");
|
||||
} else {
|
||||
addLog('No elements found to highlight', "error");
|
||||
}
|
||||
} catch (error) {
|
||||
addLog(`Highlighting failed: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
highlightElementsOnPage(elements) {
|
||||
// Remove existing highlights
|
||||
document.querySelectorAll('.opendia-highlight').forEach(el => {
|
||||
el.classList.remove('opendia-highlight');
|
||||
el.style.removeProperty('outline');
|
||||
});
|
||||
|
||||
// Add new highlights
|
||||
elements.forEach((elementData, index) => {
|
||||
try {
|
||||
const element = document.querySelector(elementData.selector);
|
||||
if (element) {
|
||||
element.classList.add('opendia-highlight');
|
||||
element.style.outline = `3px solid ${this.getHighlightColor(elementData.confidence)}`;
|
||||
element.style.outlineOffset = '2px';
|
||||
|
||||
// Add tooltip
|
||||
element.title = `OpenDia: ${elementData.name} (${Math.round(elementData.confidence * 100)}%)`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to highlight element:', elementData.selector, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-remove highlights after 10 seconds
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.opendia-highlight').forEach(el => {
|
||||
el.classList.remove('opendia-highlight');
|
||||
el.style.removeProperty('outline');
|
||||
el.style.removeProperty('outline-offset');
|
||||
el.removeAttribute('title');
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
getHighlightColor(confidence) {
|
||||
if (confidence > 0.8) return '#22c55e'; // Green for high confidence
|
||||
if (confidence > 0.6) return '#f59e0b'; // Orange for medium confidence
|
||||
return '#ef4444'; // Red for low confidence
|
||||
}
|
||||
|
||||
async sendToContentScript(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
if (tabs[0]) {
|
||||
chrome.tabs.sendMessage(tabs[0].id, message, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
} else if (response.success) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error(response.error || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(new Error('No active tab found'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
displayResults(result, testType) {
|
||||
resultArea.classList.add('show');
|
||||
this.lastResult = result; // Store for JSON viewer
|
||||
|
||||
// Display metadata with better formatting
|
||||
const method = result.data.method || 'N/A';
|
||||
const confidence = result.data.confidence ? Math.round(result.data.confidence * 100) + '%' : 'N/A';
|
||||
const elementsCount = result.data.elements ? result.data.elements.length : 0;
|
||||
|
||||
resultMeta.innerHTML = `
|
||||
<strong>✅ ${testType}</strong><br>
|
||||
<small>
|
||||
📊 Method: <code>${method}</code> |
|
||||
⏱️ Time: <code>${result.execution_time}ms</code> |
|
||||
🎯 Confidence: <code>${confidence}</code>
|
||||
${elementsCount > 0 ? ` | 🔍 Elements: <code>${elementsCount}</code>` : ''}
|
||||
</small>
|
||||
`;
|
||||
|
||||
// Display enhanced content preview
|
||||
const preview = this.createEnhancedPreview(result.data, testType);
|
||||
resultContent.innerHTML = preview;
|
||||
|
||||
// Display size info
|
||||
this.displayDataSize(result);
|
||||
|
||||
// Store full JSON for viewer
|
||||
this.updateJsonViewer(result);
|
||||
}
|
||||
|
||||
createEnhancedPreview(data, testType) {
|
||||
if (testType === 'Page Analysis' && data.elements?.length > 0) {
|
||||
return this.createElementsPreview(data.elements);
|
||||
} else if (testType === 'Content Extraction' && data.content) {
|
||||
return this.createContentPreview(data.content, data.content_type);
|
||||
} else {
|
||||
return `<div style="color: #6b7280; font-style: italic;">No relevant data found</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
createElementsPreview(elements) {
|
||||
const maxElements = 5;
|
||||
const preview = elements.slice(0, maxElements).map(e => {
|
||||
const confidenceColor = e.confidence > 0.8 ? '#22c55e' : e.confidence > 0.6 ? '#f59e0b' : '#ef4444';
|
||||
return `
|
||||
<div style="margin: 4px 0; padding: 6px; background: #f9fafb; border-radius: 4px; border-left: 3px solid ${confidenceColor};">
|
||||
<strong>${e.name}</strong>
|
||||
<div style="font-size: 0.65rem; color: #6b7280; margin-top: 2px;">
|
||||
Type: ${e.type} | Confidence: ${Math.round(e.confidence * 100)}% | ID: ${e.id}
|
||||
</div>
|
||||
<div style="font-size: 0.6rem; color: #9ca3af; font-family: monospace; margin-top: 2px;">
|
||||
${e.selector}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const remaining = elements.length - maxElements;
|
||||
const remainingText = remaining > 0 ? `<div style="color: #6b7280; font-size: 0.7rem; margin-top: 8px;">+ ${remaining} more elements...</div>` : '';
|
||||
|
||||
return preview + remainingText;
|
||||
}
|
||||
|
||||
createContentPreview(content, contentType) {
|
||||
if (typeof content === 'object') {
|
||||
if (content.title) {
|
||||
return `
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong>📰 ${content.title}</strong>
|
||||
</div>
|
||||
<div style="font-size: 0.65rem; color: #6b7280;">
|
||||
${content.word_count ? `📝 Words: ${content.word_count}` : ''}
|
||||
${content.reading_time ? ` | ⏱️ Read time: ${content.reading_time}min` : ''}
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 0.7rem; max-height: 60px; overflow: hidden;">
|
||||
${(content.content || '').substring(0, 200)}${content.content?.length > 200 ? '...' : ''}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `<pre style="font-size: 0.65rem; max-height: 80px; overflow: hidden;">${JSON.stringify(content, null, 2).substring(0, 300)}</pre>`;
|
||||
}
|
||||
} else {
|
||||
return `<div style="max-height: 80px; overflow: hidden; font-size: 0.7rem;">${content.substring(0, 300)}${content.length > 300 ? '...' : ''}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
displayDataSize(result) {
|
||||
const dataSize = result.data_size;
|
||||
const readableSize = this.formatBytes(dataSize);
|
||||
const tokenEstimate = Math.round(dataSize / 4); // Rough token estimate
|
||||
|
||||
dataSizeInfo.innerHTML = `
|
||||
<span class="size-label">Data Size:</span>
|
||||
<span class="size-value ${dataSize > 10000 ? 'large' : 'normal'}">${readableSize}</span>
|
||||
<span class="compression-info">(~${tokenEstimate} tokens)</span>
|
||||
`;
|
||||
}
|
||||
|
||||
formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
toggleJsonViewer() {
|
||||
if (jsonViewer.style.display === 'none') {
|
||||
jsonViewer.style.display = 'block';
|
||||
expandButton.textContent = '📄 Hide JSON';
|
||||
resultArea.classList.add('test-results-expanded');
|
||||
} else {
|
||||
jsonViewer.style.display = 'none';
|
||||
expandButton.textContent = '📄 View Full JSON';
|
||||
resultArea.classList.remove('test-results-expanded');
|
||||
}
|
||||
}
|
||||
|
||||
updateJsonViewer(result) {
|
||||
const formattedJson = this.formatJson(result);
|
||||
jsonViewer.textContent = formattedJson;
|
||||
}
|
||||
|
||||
formatJson(obj) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize testing interface
|
||||
const testingInterface = new TestingInterface();
|
||||
|
||||
// Add log entry
|
||||
function addLog(message, type = "info") {
|
||||
const entry = document.createElement("div");
|
||||
@@ -109,4 +400,4 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
} else if (message.type === "log") {
|
||||
addLog(message.message, message.type || "info");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user