feat: Integrated Vision & Robust Translation Layer, Secured Repo (removed keys)
This commit is contained in:
219
qwen-automation-extension/background.js
Normal file
219
qwen-automation-extension/background.js
Normal file
@@ -0,0 +1,219 @@
|
||||
// Background script for Qwen AI Automation Extension
|
||||
let isAuthenticated = false;
|
||||
let qwenToken = null;
|
||||
|
||||
// Handle extension installation
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
console.log('Qwen AI Automation Extension installed');
|
||||
});
|
||||
|
||||
// Handle messages from popup
|
||||
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
|
||||
switch (message.action) {
|
||||
case 'checkAuth':
|
||||
sendResponse({ authenticated: isAuthenticated });
|
||||
break;
|
||||
|
||||
case 'openAuth':
|
||||
// Open Qwen authentication in a new tab
|
||||
try {
|
||||
await chrome.tabs.create({
|
||||
url: 'https://chat.qwen.ai'
|
||||
});
|
||||
sendResponse({ success: true });
|
||||
} catch (error) {
|
||||
sendResponse({ success: false, error: error.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'executeTask':
|
||||
if (!isAuthenticated) {
|
||||
sendResponse({ error: 'Not authenticated with Qwen' });
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeBrowserTask(message.task);
|
||||
sendResponse({ success: true, result: result });
|
||||
} catch (error) {
|
||||
sendResponse({ success: false, error: error.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'updateAuthStatus':
|
||||
isAuthenticated = message.authenticated;
|
||||
qwenToken = message.token || null;
|
||||
|
||||
// Notify popup about auth status change
|
||||
chrome.runtime.sendMessage({ action: 'authStatusUpdated' });
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
}
|
||||
|
||||
return true; // Keep message channel open for async response
|
||||
});
|
||||
|
||||
// Execute browser automation task
|
||||
async function executeBrowserTask(task) {
|
||||
// Get current active tab
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
});
|
||||
|
||||
if (!activeTab) {
|
||||
throw new Error('No active tab found');
|
||||
}
|
||||
|
||||
try {
|
||||
// Analyze the task and determine appropriate automation steps
|
||||
const automationSteps = await analyzeTaskWithQwen(task, activeTab.url);
|
||||
|
||||
// Execute each step
|
||||
let results = [];
|
||||
for (const step of automationSteps) {
|
||||
const result = await executeAutomationStep(step, activeTab.id);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return `Task completed successfully. Performed ${automationSteps.length} steps.`;
|
||||
} catch (error) {
|
||||
throw new Error(`Task execution failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze task with Qwen AI (simplified for this example)
|
||||
async function analyzeTaskWithQwen(task, currentUrl) {
|
||||
// This would normally call the Qwen API
|
||||
// For now, we'll use a simple rule-based approach
|
||||
// In a real implementation, this would send the task to Qwen API
|
||||
|
||||
console.log(`Analyzing task: ${task} on page: ${currentUrl}`);
|
||||
|
||||
// Simple rule-based analysis (would be replaced with Qwen API call)
|
||||
if (task.toLowerCase().includes('search') || task.toLowerCase().includes('google')) {
|
||||
return [
|
||||
{
|
||||
action: 'fill',
|
||||
selector: 'textarea[name="q"], input[name="q"], [name="search"], #search',
|
||||
value: extractSearchQuery(task)
|
||||
},
|
||||
{
|
||||
action: 'press',
|
||||
key: 'Enter'
|
||||
}
|
||||
];
|
||||
} else if (task.toLowerCase().includes('click') || task.toLowerCase().includes('click on')) {
|
||||
const element = extractElementFromTask(task);
|
||||
return [
|
||||
{
|
||||
action: 'click',
|
||||
selector: element
|
||||
}
|
||||
];
|
||||
} else {
|
||||
// Default: just return the task as is for Qwen to process
|
||||
return [
|
||||
{
|
||||
action: 'analyze',
|
||||
task: task,
|
||||
url: currentUrl
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract search query from task
|
||||
function extractSearchQuery(task) {
|
||||
const match = task.match(/search for ["']?([^"']+)["']?/i) ||
|
||||
task.match(/google ["']?([^"']+)["']?/i) ||
|
||||
task.match(/find ["']?([^"']+)["']?/i);
|
||||
return match ? match[1] : task.replace(/(search|google|find)\s*/i, '').trim();
|
||||
}
|
||||
|
||||
// Extract element from task
|
||||
function extractElementFromTask(task) {
|
||||
// Simple extraction - in reality would be more sophisticated
|
||||
const lowerTask = task.toLowerCase();
|
||||
if (lowerTask.includes('search') || lowerTask.includes('google')) return 'textarea[name="q"], input[name="q"]';
|
||||
if (lowerTask.includes('button')) return 'button';
|
||||
if (lowerTask.includes('link')) return 'a';
|
||||
return '*'; // generic selector
|
||||
}
|
||||
|
||||
// Execute a single automation step
|
||||
async function executeAutomationStep(step, tabId) {
|
||||
try {
|
||||
switch (step.action) {
|
||||
case 'click':
|
||||
return await chrome.scripting.executeScript({
|
||||
target: { tabId: tabId },
|
||||
func: clickElement,
|
||||
args: [step.selector]
|
||||
});
|
||||
|
||||
case 'fill':
|
||||
return await chrome.scripting.executeScript({
|
||||
target: { tabId: tabId },
|
||||
func: fillElement,
|
||||
args: [step.selector, step.value]
|
||||
});
|
||||
|
||||
case 'press':
|
||||
// For key press, we'll inject a script to simulate the key
|
||||
return await chrome.scripting.executeScript({
|
||||
target: { tabId: tabId },
|
||||
func: pressKey,
|
||||
args: [step.key]
|
||||
});
|
||||
|
||||
default:
|
||||
console.log('Unknown action:', step.action);
|
||||
return { success: false, error: `Unknown action: ${step.action}` };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Step execution error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to be injected into the page
|
||||
function clickElement(selector) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.click();
|
||||
return { success: true, message: `Clicked element: ${selector}` };
|
||||
} else {
|
||||
return { success: false, error: `Element not found: ${selector}` };
|
||||
}
|
||||
}
|
||||
|
||||
function fillElement(selector, value) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return { success: true, message: `Filled element: ${selector} with value: ${value}` };
|
||||
} else {
|
||||
return { success: false, error: `Element not found: ${selector}` };
|
||||
}
|
||||
}
|
||||
|
||||
function pressKey(key) {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: key,
|
||||
code: key.toUpperCase(),
|
||||
bubbles: true
|
||||
});
|
||||
document.activeElement.dispatchEvent(event);
|
||||
return { success: true, message: `Pressed key: ${key}` };
|
||||
}
|
||||
|
||||
// Listen for tab updates to manage state
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
if (changeInfo.status === 'complete' && tab.active) {
|
||||
// Tab loaded completely, extension is ready for new tasks
|
||||
console.log(`Tab ${tabId} loaded: ${tab.url}`);
|
||||
}
|
||||
});
|
||||
201
qwen-automation-extension/content.js
Normal file
201
qwen-automation-extension/content.js
Normal file
@@ -0,0 +1,201 @@
|
||||
// Content script for Qwen AI Automation Extension
|
||||
console.log('Qwen AI Automation content script loaded');
|
||||
|
||||
// Store extension state
|
||||
let extensionState = {
|
||||
isActive: false,
|
||||
currentTask: null,
|
||||
qwenToken: null
|
||||
};
|
||||
|
||||
// Listen for messages from background script
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
switch (message.action) {
|
||||
case 'getPageContent':
|
||||
sendResponse(getPageContent());
|
||||
break;
|
||||
|
||||
case 'getElementInfo':
|
||||
sendResponse(getElementInfo(message.selector));
|
||||
break;
|
||||
|
||||
case 'executeAction':
|
||||
sendResponse(executeAction(message.action, message.params));
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown message action:', message.action);
|
||||
}
|
||||
|
||||
return true; // Keep message channel open for async response
|
||||
});
|
||||
|
||||
// Get page content for AI analysis
|
||||
function getPageContent() {
|
||||
return {
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
content: document.body.innerText.substring(0, 2000), // First 2000 chars
|
||||
elements: Array.from(document.querySelectorAll('input, button, a, textarea, select'))
|
||||
.map(el => ({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || null,
|
||||
className: el.className || null,
|
||||
text: el.textContent?.substring(0, 100) || el.value || null,
|
||||
name: el.name || null,
|
||||
placeholder: el.placeholder || null,
|
||||
role: el.getAttribute('role') || null
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// Get specific element information
|
||||
function getElementInfo(selector) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
return {
|
||||
exists: true,
|
||||
tag: element.tagName.toLowerCase(),
|
||||
id: element.id || null,
|
||||
className: element.className || null,
|
||||
text: element.textContent?.substring(0, 100) || element.value || null,
|
||||
name: element.name || null,
|
||||
placeholder: element.placeholder || null,
|
||||
role: element.getAttribute('role') || null,
|
||||
rect: element.getBoundingClientRect(),
|
||||
isVisible: !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length)
|
||||
};
|
||||
} else {
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
// Execute an action on the page
|
||||
function executeAction(action, params) {
|
||||
try {
|
||||
switch (action) {
|
||||
case 'click':
|
||||
return clickElement(params.selector);
|
||||
|
||||
case 'fill':
|
||||
return fillElement(params.selector, params.value);
|
||||
|
||||
case 'clickText':
|
||||
return clickElementByText(params.text);
|
||||
|
||||
case 'waitForElement':
|
||||
return waitForElement(params.selector, params.timeout || 5000);
|
||||
|
||||
case 'scrollToElement':
|
||||
return scrollToElement(params.selector);
|
||||
|
||||
case 'extractText':
|
||||
return extractTextFromElement(params.selector);
|
||||
|
||||
default:
|
||||
return { success: false, error: `Unknown action: ${action}` };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function clickElement(selector) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.click();
|
||||
return { success: true, message: `Clicked element: ${selector}` };
|
||||
} else {
|
||||
return { success: false, error: `Element not found: ${selector}` };
|
||||
}
|
||||
}
|
||||
|
||||
function fillElement(selector, value) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('blur', { bubbles: true })); // Trigger any blur events
|
||||
return { success: true, message: `Filled element: ${selector} with value: ${value}` };
|
||||
} else {
|
||||
return { success: false, error: `Element not found: ${selector}` };
|
||||
}
|
||||
}
|
||||
|
||||
function clickElementByText(text) {
|
||||
const elements = Array.from(document.querySelectorAll('button, a, input, textarea, span, div'));
|
||||
const element = elements.find(el =>
|
||||
el.textContent?.trim().toLowerCase().includes(text.toLowerCase()) ||
|
||||
el.value?.toLowerCase().includes(text.toLowerCase()) ||
|
||||
el.placeholder?.toLowerCase().includes(text.toLowerCase())
|
||||
);
|
||||
|
||||
if (element) {
|
||||
element.click();
|
||||
return { success: true, message: `Clicked element with text: ${text}` };
|
||||
} else {
|
||||
return { success: false, error: `Element with text not found: ${text}` };
|
||||
}
|
||||
}
|
||||
|
||||
function waitForElement(selector, timeout) {
|
||||
return new Promise((resolve) => {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
resolve({ success: true, message: `Element found immediately: ${selector}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
observer.disconnect();
|
||||
resolve({ success: true, message: `Element found after waiting: ${selector}` });
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve({ success: false, error: `Element not found within timeout: ${selector}` });
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
function scrollToElement(selector) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return { success: true, message: `Scrolled to element: ${selector}` };
|
||||
} else {
|
||||
return { success: false, error: `Element not found: ${selector}` };
|
||||
}
|
||||
}
|
||||
|
||||
function extractTextFromElement(selector) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
return {
|
||||
success: true,
|
||||
text: element.textContent || element.value || '',
|
||||
message: `Extracted text from element: ${selector}`
|
||||
};
|
||||
} else {
|
||||
return { success: false, error: `Element not found: ${selector}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Expose functions to window for advanced usage if needed
|
||||
window.qwenAutomation = {
|
||||
getPageContent,
|
||||
getElementInfo,
|
||||
executeAction,
|
||||
clickElement,
|
||||
fillElement
|
||||
};
|
||||
1
qwen-automation-extension/icon128.png
Normal file
1
qwen-automation-extension/icon128.png
Normal file
@@ -0,0 +1 @@
|
||||
This is a placeholder for the 128x128 icon file. In a real extension, this would be an actual PNG image file.
|
||||
1
qwen-automation-extension/icon48.png
Normal file
1
qwen-automation-extension/icon48.png
Normal file
@@ -0,0 +1 @@
|
||||
This is a placeholder for the 48x48 icon file. In a real extension, this would be an actual PNG image file.
|
||||
32
qwen-automation-extension/manifest.json
Normal file
32
qwen-automation-extension/manifest.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Qwen AI Automation Suite",
|
||||
"version": "1.0.0",
|
||||
"description": "AI-powered browser automation with Qwen integration",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"scripting",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Qwen AI Automation"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"16": "icon16.png",
|
||||
"48": "icon48.png",
|
||||
"128": "icon128.png"
|
||||
}
|
||||
}
|
||||
121
qwen-automation-extension/popup.html
Normal file
121
qwen-automation-extension/popup.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
width: 350px;
|
||||
padding: 15px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
color: #1a73e8;
|
||||
}
|
||||
.auth-section {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.auth-status {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.authenticated {
|
||||
background-color: #e6f4ea;
|
||||
color: #137333;
|
||||
}
|
||||
.not-authenticated {
|
||||
background-color: #fce8e6;
|
||||
color: #c5221f;
|
||||
}
|
||||
.task-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.execute-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #1a73e8;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.execute-btn:hover {
|
||||
background-color: #0d62c9;
|
||||
}
|
||||
.execute-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.history {
|
||||
margin-top: 15px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.history-item {
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 12px;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
.spinner {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #1a73e8;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🤖 Qwen AI Automation</h1>
|
||||
</div>
|
||||
|
||||
<div class="auth-section">
|
||||
<div id="authStatus" class="auth-status not-authenticated">
|
||||
Not authenticated with Qwen
|
||||
</div>
|
||||
<button id="authBtn" class="execute-btn">Authenticate with Qwen</button>
|
||||
</div>
|
||||
|
||||
<div id="taskSection" style="display: none;">
|
||||
<textarea id="taskInput" class="task-input" rows="3" placeholder="Describe your automation task..."></textarea>
|
||||
<button id="executeBtn" class="execute-btn">Execute Task</button>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<div>Processing with AI...</div>
|
||||
</div>
|
||||
|
||||
<div class="history" id="history">
|
||||
<h3>Recent Tasks</h3>
|
||||
<div id="historyList"></div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
91
qwen-automation-extension/popup.js
Normal file
91
qwen-automation-extension/popup.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// Popup UI Logic
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const authStatus = document.getElementById('authStatus');
|
||||
const authBtn = document.getElementById('authBtn');
|
||||
const taskSection = document.getElementById('taskSection');
|
||||
const taskInput = document.getElementById('taskInput');
|
||||
const executeBtn = document.getElementById('executeBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
const historyList = document.getElementById('historyList');
|
||||
|
||||
// Check authentication status
|
||||
checkAuthStatus();
|
||||
|
||||
// Auth button click handler
|
||||
authBtn.addEventListener('click', async function() {
|
||||
try {
|
||||
// Open authentication flow
|
||||
await chrome.runtime.sendMessage({ action: 'openAuth' });
|
||||
} catch (error) {
|
||||
console.error('Auth error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Execute button click handler
|
||||
executeBtn.addEventListener('click', async function() {
|
||||
const task = taskInput.value.trim();
|
||||
if (!task) return;
|
||||
|
||||
// Show loading
|
||||
executeBtn.disabled = true;
|
||||
loading.style.display = 'block';
|
||||
|
||||
try {
|
||||
// Send task to background script
|
||||
const result = await chrome.runtime.sendMessage({
|
||||
action: 'executeTask',
|
||||
task: task
|
||||
});
|
||||
|
||||
// Add to history
|
||||
addToHistory(task, result);
|
||||
taskInput.value = '';
|
||||
} catch (error) {
|
||||
console.error('Execution error:', error);
|
||||
addToHistory(task, `Error: ${error.message}`);
|
||||
} finally {
|
||||
// Hide loading
|
||||
executeBtn.disabled = false;
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
async function checkAuthStatus() {
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({ action: 'checkAuth' });
|
||||
if (response.authenticated) {
|
||||
authStatus.textContent = '✅ Authenticated with Qwen';
|
||||
authStatus.className = 'auth-status authenticated';
|
||||
taskSection.style.display = 'block';
|
||||
} else {
|
||||
authStatus.textContent = '❌ Not authenticated with Qwen';
|
||||
authStatus.className = 'auth-status not-authenticated';
|
||||
taskSection.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function addToHistory(task, result) {
|
||||
const historyItem = document.createElement('div');
|
||||
historyItem.className = 'history-item';
|
||||
historyItem.innerHTML = `
|
||||
<strong>Task:</strong> ${task}<br>
|
||||
<strong>Result:</strong> ${result}
|
||||
`;
|
||||
historyList.insertBefore(historyItem, historyList.firstChild);
|
||||
|
||||
// Limit to 5 items
|
||||
if (historyList.children.length > 5) {
|
||||
historyList.removeChild(historyList.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for auth status updates
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'authStatusUpdated') {
|
||||
checkAuthStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user