123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- import 'webextension-polyfill';
- import { agentModelStore, AgentNameEnum, generalSettingsStore, llmProviderStore } from '@extension/storage';
- import BrowserContext from './browser/context';
- import { Executor } from './agent/executor';
- import { createLogger } from './log';
- import { ExecutionState } from './agent/event/types';
- import { createChatModel } from './agent/helper';
- const logger = createLogger('background');
- const browserContext = new BrowserContext({});
- let currentExecutor: Executor | null = null;
- let currentPort: chrome.runtime.Port | null = null;
- // Setup side panel behavior
- chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(error => console.error(error));
- // Function to check if script is already injected
- async function isScriptInjected(tabId: number): Promise<boolean> {
- try {
- const results = await chrome.scripting.executeScript({
- target: { tabId },
- func: () => Object.prototype.hasOwnProperty.call(window, 'buildDomTree'),
- });
- return results[0]?.result || false;
- } catch (err) {
- console.error('Failed to check script injection status:', err);
- return false;
- }
- }
- // // Function to inject the buildDomTree script
- async function injectBuildDomTree(tabId: number) {
- try {
- // Check if already injected
- const alreadyInjected = await isScriptInjected(tabId);
- if (alreadyInjected) {
- console.log('Scripts already injected, skipping...');
- return;
- }
- await chrome.scripting.executeScript({
- target: { tabId },
- files: ['buildDomTree.js'],
- });
- console.log('Scripts successfully injected');
- } catch (err) {
- console.error('Failed to inject scripts:', err);
- }
- }
- chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
- if (tabId && changeInfo.status === 'complete' && tab.url?.startsWith('http')) {
- await injectBuildDomTree(tabId);
- }
- });
- // Listen for debugger detached event
- // if canceled_by_user, remove the tab from the browser context
- chrome.debugger.onDetach.addListener(async (source, reason) => {
- console.log('Debugger detached:', source, reason);
- if (reason === 'canceled_by_user') {
- if (source.tabId) {
- await browserContext.cleanup();
- }
- }
- });
- // Cleanup when tab is closed
- chrome.tabs.onRemoved.addListener(tabId => {
- browserContext.removeAttachedPage(tabId);
- });
- logger.info('background loaded');
- // Setup connection listener
- chrome.runtime.onConnect.addListener(port => {
- if (port.name === 'side-panel-connection') {
- currentPort = port;
- port.onMessage.addListener(async message => {
- try {
- switch (message.type) {
- case 'heartbeat':
- // Acknowledge heartbeat
- port.postMessage({ type: 'heartbeat_ack' });
- break;
- case 'new_task': {
- if (!message.task) return port.postMessage({ type: 'error', error: 'No task provided' });
- if (!message.tabId) return port.postMessage({ type: 'error', error: 'No tab ID provided' });
- logger.info('new_task', message.tabId, message.task);
- currentExecutor = await setupExecutor(message.taskId, message.task, browserContext);
- subscribeToExecutorEvents(currentExecutor);
- const result = await currentExecutor.execute();
- logger.info('new_task execution result', message.tabId, result);
- break;
- }
- case 'follow_up_task': {
- if (!message.task) return port.postMessage({ type: 'error', error: 'No follow up task provided' });
- if (!message.tabId) return port.postMessage({ type: 'error', error: 'No tab ID provided' });
- logger.info('follow_up_task', message.tabId, message.task);
- // If executor exists, add follow-up task
- if (currentExecutor) {
- currentExecutor.addFollowUpTask(message.task);
- // Re-subscribe to events in case the previous subscription was cleaned up
- subscribeToExecutorEvents(currentExecutor);
- const result = await currentExecutor.execute();
- logger.info('follow_up_task execution result', message.tabId, result);
- } else {
- // executor was cleaned up, can not add follow-up task
- logger.info('follow_up_task: executor was cleaned up, can not add follow-up task');
- return port.postMessage({ type: 'error', error: 'Executor was cleaned up, can not add follow-up task' });
- }
- break;
- }
- case 'cancel_task': {
- if (!currentExecutor) return port.postMessage({ type: 'error', error: 'No task to cancel' });
- await currentExecutor.cancel();
- break;
- }
- case 'screenshot': {
- if (!message.tabId) return port.postMessage({ type: 'error', error: 'No tab ID provided' });
- const page = await browserContext.switchTab(message.tabId);
- const screenshot = await page.takeScreenshot();
- logger.info('screenshot', message.tabId, screenshot);
- return port.postMessage({ type: 'success', screenshot });
- }
- case 'resume_task': {
- if (!currentExecutor) return port.postMessage({ type: 'error', error: 'No task to resume' });
- await currentExecutor.resume();
- return port.postMessage({ type: 'success' });
- }
- case 'pause_task': {
- if (!currentExecutor) return port.postMessage({ type: 'error', error: 'No task to pause' });
- await currentExecutor.pause();
- return port.postMessage({ type: 'success' });
- }
- default:
- return port.postMessage({ type: 'error', error: 'Unknown message type' });
- }
- } catch (error) {
- console.error('Error handling port message:', error);
- port.postMessage({
- type: 'error',
- error: error instanceof Error ? error.message : 'Unknown error',
- });
- }
- });
- port.onDisconnect.addListener(() => {
- console.log('Side panel disconnected');
- currentPort = null;
- });
- }
- });
- async function setupExecutor(taskId: string, task: string, browserContext: BrowserContext) {
- const providers = await llmProviderStore.getAllProviders();
- // if no providers, need to display the options page
- if (Object.keys(providers).length === 0) {
- throw new Error('Please configure API keys in the settings first');
- }
- const agentModels = await agentModelStore.getAllAgentModels();
- // verify if every provider used in the agent models exists in the providers
- for (const agentModel of Object.values(agentModels)) {
- if (!providers[agentModel.provider]) {
- throw new Error(`Provider ${agentModel.provider} not found in the settings`);
- }
- }
- const navigatorModel = agentModels[AgentNameEnum.Navigator];
- if (!navigatorModel) {
- throw new Error('Please choose a model for the navigator in the settings first');
- }
- const navigatorLLM = createChatModel(providers[navigatorModel.provider], navigatorModel);
- let plannerLLM = null;
- const plannerModel = agentModels[AgentNameEnum.Planner];
- if (plannerModel) {
- plannerLLM = createChatModel(providers[plannerModel.provider], plannerModel);
- }
- let validatorLLM = null;
- const validatorModel = agentModels[AgentNameEnum.Validator];
- if (validatorModel) {
- validatorLLM = createChatModel(providers[validatorModel.provider], validatorModel);
- }
- const generalSettings = await generalSettingsStore.getSettings();
- const executor = new Executor(task, taskId, browserContext, navigatorLLM, {
- plannerLLM: plannerLLM ?? navigatorLLM,
- validatorLLM: validatorLLM ?? navigatorLLM,
- agentOptions: {
- maxSteps: generalSettings.maxSteps,
- maxFailures: generalSettings.maxFailures,
- maxActionsPerStep: generalSettings.maxActionsPerStep,
- useVision: generalSettings.useVision,
- useVisionForPlanner: generalSettings.useVisionForPlanner,
- planningInterval: generalSettings.planningInterval,
- },
- });
- return executor;
- }
- // Update subscribeToExecutorEvents to use port
- async function subscribeToExecutorEvents(executor: Executor) {
- // Clear previous event listeners to prevent multiple subscriptions
- executor.clearExecutionEvents();
- // Subscribe to new events
- executor.subscribeExecutionEvents(async event => {
- try {
- if (currentPort) {
- currentPort.postMessage(event);
- }
- } catch (error) {
- logger.error('Failed to send message to side panel:', error);
- }
- if (
- event.state === ExecutionState.TASK_OK ||
- event.state === ExecutionState.TASK_FAIL ||
- event.state === ExecutionState.TASK_CANCEL
- ) {
- await currentExecutor?.cleanup();
- }
- });
- }
|