From 2f6d28a3e94bcf24193cd0ace84e3465a2bb4805 Mon Sep 17 00:00:00 2001 From: jackwener Date: Thu, 19 Mar 2026 23:59:31 +0800 Subject: [PATCH] feat(extension): auto-close automation window after command completes - Add 'close-window' action to extension protocol and background.ts - Add Page.closeWindow() method to send close-window command - browserSession() now closes automation window in cleanup - Remove domain pre-navigation + 2s wait from main.ts (CDP handles cross-domain cookies natively, no same-origin workaround needed) - Net effect: commands run faster, no stale windows left behind --- extension/dist/background.js | 14 ++++++++++++++ extension/src/background.ts | 14 ++++++++++++++ extension/src/protocol.ts | 2 +- src/browser/daemon-client.ts | 2 +- src/browser/page.ts | 9 +++++++++ src/main.ts | 6 ------ src/runtime.ts | 12 +++++++----- 7 files changed, 46 insertions(+), 13 deletions(-) diff --git a/extension/dist/background.js b/extension/dist/background.js index 656c5d0..e3605fb 100644 --- a/extension/dist/background.js +++ b/extension/dist/background.js @@ -219,6 +219,7 @@ async function handleCommand(cmd) { case "tabs": return await handleTabs(cmd); case "cookies": return await handleCookies(cmd); case "screenshot": return await handleScreenshot(cmd); + case "close-window": return await handleCloseWindow(cmd); default: return { id: cmd.id, ok: false, @@ -447,4 +448,17 @@ async function handleScreenshot(cmd) { }; } } +async function handleCloseWindow(cmd) { + if (automationWindowId !== null) { + try { + await chrome.windows.remove(automationWindowId); + } catch {} + automationWindowId = null; + } + return { + id: cmd.id, + ok: true, + data: { closed: true } + }; +} //#endregion diff --git a/extension/src/background.ts b/extension/src/background.ts index dfd0962..ba64fe1 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -165,6 +165,8 @@ async function handleCommand(cmd: Command): Promise { return await handleCookies(cmd); case 'screenshot': return await handleScreenshot(cmd); + case 'close-window': + return await handleCloseWindow(cmd); default: return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` }; } @@ -332,3 +334,15 @@ async function handleScreenshot(cmd: Command): Promise { return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; } } + +async function handleCloseWindow(cmd: Command): Promise { + if (automationWindowId !== null) { + try { + await chrome.windows.remove(automationWindowId); + } catch { + // Window may already be closed + } + automationWindowId = null; + } + return { id: cmd.id, ok: true, data: { closed: true } }; +} diff --git a/extension/src/protocol.ts b/extension/src/protocol.ts index efce9c0..8e9f19c 100644 --- a/extension/src/protocol.ts +++ b/extension/src/protocol.ts @@ -5,7 +5,7 @@ * Everything else is just JS code sent via 'exec'. */ -export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot'; +export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window'; export interface Command { /** Unique request ID */ diff --git a/src/browser/daemon-client.ts b/src/browser/daemon-client.ts index 8d7b935..6239cb7 100644 --- a/src/browser/daemon-client.ts +++ b/src/browser/daemon-client.ts @@ -15,7 +15,7 @@ function generateId(): string { export interface DaemonCommand { id: string; - action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot'; + action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window'; tabId?: number; code?: string; url?: string; diff --git a/src/browser/page.ts b/src/browser/page.ts index fafc01d..ff5be2a 100644 --- a/src/browser/page.ts +++ b/src/browser/page.ts @@ -37,6 +37,15 @@ export class Page implements IPage { } } + /** Close the automation window in the extension */ + async closeWindow(): Promise { + try { + await sendCommand('close-window', {}); + } catch { + // Window may already be closed or daemon may be down + } + } + async evaluate(js: string): Promise { const code = wrapForEval(js); return sendCommand('exec', { code, ...this._tabOpt() }); diff --git a/src/main.ts b/src/main.ts index eb825a4..e59588a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -193,12 +193,6 @@ for (const [, cmd] of registry) { let result: any; if (cmd.browser) { result = await browserSession(BrowserBridge, async (page) => { - // Cookie/header strategies require same-origin context for credentialed fetch. - // In CDP mode the active tab may be on an unrelated domain, causing CORS failures. - // Navigate to the command's domain first (mirrors cascade command behavior). - if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) { - try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {} - } return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }); }); } else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); } diff --git a/src/runtime.ts b/src/runtime.ts index 6bd6154..9677e12 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,8 +1,5 @@ -/** - * Runtime utilities: timeouts and browser session management. - */ - import type { IPage } from './types.js'; +import type { Page } from './browser/page.js'; export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10); export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10); @@ -45,7 +42,12 @@ export async function browserSession( const mcp = new BrowserFactory(); try { const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT }); - return await fn(page); + const result = await fn(page); + // Close the automation window after command completes + if (typeof (page as Page).closeWindow === 'function') { + await (page as Page).closeWindow(); + } + return result; } finally { await mcp.close().catch(() => {}); }