UNPKG

@ai-coding-labs/playwright-mcp-plus

Version:

Enhanced Playwright Tools for MCP with Project Session Isolation

214 lines (213 loc) 10.5 kB
/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import * as playwright from 'playwright'; import { testDebug } from './log.js'; import { EnhancedProjectIsolationManager } from './enhancedProjectIsolation.js'; import { getInstalledExtensionPaths } from './tools/extensions.js'; /** * Enhance launch options with MCP-managed extensions */ function enhanceLaunchOptionsWithExtensions(launchOptions, sessionUserDataDir) { const mcpExtensionPaths = getInstalledExtensionPaths(sessionUserDataDir); const enhancedOptions = { ...launchOptions }; let args = [...(enhancedOptions.args || [])]; // Collect all extension paths (existing + MCP-managed) const allExtensionPaths = []; // 1. Find and remove existing --load-extension and --disable-extensions-except arguments, extract paths for (let i = args.length - 1; i >= 0; i--) { const arg = args[i]; if (arg.startsWith('--load-extension=')) { const existingPaths = arg.substring('--load-extension='.length).split(','); allExtensionPaths.push(...existingPaths.filter(path => path.trim())); args.splice(i, 1); // Remove existing argument testDebug(`Found existing --load-extension argument: ${existingPaths.join(',')}`); } else if (arg.startsWith('--disable-extensions-except=')) { const existingPaths = arg.substring('--disable-extensions-except='.length).split(','); allExtensionPaths.push(...existingPaths.filter(path => path.trim())); args.splice(i, 1); // Remove existing argument testDebug(`Found existing --disable-extensions-except argument: ${existingPaths.join(',')}`); } } // 2. Add MCP-managed extension paths allExtensionPaths.push(...mcpExtensionPaths); // 3. Remove duplicates and empty paths const uniqueExtensionPaths = [...new Set(allExtensionPaths.filter(path => path.trim()))]; // 4. If we have extension paths, add extension arguments if (uniqueExtensionPaths.length > 0) { const allExtensionPathsStr = uniqueExtensionPaths.join(','); // Remove any --disable-extensions argument that might conflict args = args.filter(arg => arg !== '--disable-extensions'); // According to Playwright docs and Chromium behavior: // - We need both --disable-extensions-except and --load-extension // - --disable-extensions-except will override the default --disable-extensions that Playwright adds // - Place these arguments at the END so they override any earlier conflicting arguments args.push(`--disable-extensions-except=${allExtensionPathsStr}`); args.push(`--load-extension=${allExtensionPathsStr}`); testDebug(`Enhanced launch options with ${uniqueExtensionPaths.length} total extensions: ${allExtensionPathsStr}`); } else { testDebug('No extensions to load'); } enhancedOptions.args = args; return enhancedOptions; } /** * 增强的持久化上下文工厂 * 扩展原有PersistentContextFactory,支持新的会话目录管理策略 */ export class EnhancedPersistentContextFactory { browserConfig; config; _userDataDirs = new Set(); constructor(browserConfig, config) { this.browserConfig = browserConfig; this.config = config; } async createContext(clientInfo, projectInfo) { testDebug('create browser context (enhanced persistent)'); // 使用增强的用户数据目录创建逻辑 const userDataDir = this.browserConfig.userDataDir ?? await this._createEnhancedUserDataDir(projectInfo); this._userDataDirs.add(userDataDir); testDebug('lock user data dir (enhanced)', userDataDir); const browserType = playwright[this.browserConfig.browserName]; const enhancedLaunchOptions = enhanceLaunchOptionsWithExtensions(this.browserConfig.launchOptions || {}, userDataDir); for (let i = 0; i < 5; i++) { try { const browserContext = await browserType.launchPersistentContext(userDataDir, { ...enhancedLaunchOptions, ...this.browserConfig.contextOptions, handleSIGINT: false, handleSIGTERM: false, }); const close = () => this._closeBrowserContext(browserContext, userDataDir); return { browserContext, close, userDataDir }; } catch (error) { if (error.message.includes('Executable doesn\'t exist')) throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) { // User data directory is already in use, try again. await new Promise(resolve => setTimeout(resolve, 1000)); continue; } throw error; } } throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); } async _closeBrowserContext(browserContext, userDataDir) { await browserContext.close(); this._userDataDirs.delete(userDataDir); testDebug('unlock user data dir (enhanced)', userDataDir); testDebug('close browser context complete (enhanced persistent)'); } /** * 增强的用户数据目录创建逻辑 * 集成新的SessionDirectoryManager和原有逻辑 */ async _createEnhancedUserDataDir(projectInfo) { try { // 尝试使用增强的项目隔离管理器 if (this.config.projectIsolation && projectInfo) { const enhancedUserDataDir = await EnhancedProjectIsolationManager.createUserDataDir(this.config, { projectDrive: projectInfo.projectDrive, projectPath: projectInfo.projectPath }); if (enhancedUserDataDir) { testDebug('using enhanced user data dir', enhancedUserDataDir); return enhancedUserDataDir; } } // 降级到原有逻辑 return await this._createDefaultUserDataDir(); } catch (error) { // Enhanced user data dir creation failed, falling back to default return await this._createDefaultUserDataDir(); } } /** * 原有的默认用户数据目录创建逻辑 * 保持与原PersistentContextFactory一致 */ async _createDefaultUserDataDir() { let cacheDirectory; if (process.platform === 'linux') cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); else if (process.platform === 'darwin') cacheDirectory = path.join(os.homedir(), 'Library', 'Caches'); else if (process.platform === 'win32') cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); else throw new Error('Unsupported platform: ' + process.platform); const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${this.browserConfig.launchOptions?.channel ?? this.browserConfig?.browserName}-profile`); await fs.promises.mkdir(result, { recursive: true }); testDebug('using default user data dir', result); return result; } /** * 获取会话目录信息(用于调试) */ getSessionInfo(projectInfo) { if (!this.config.projectIsolation || !projectInfo) return { enhanced: false }; const info = EnhancedProjectIsolationManager.getSessionDirectoryInfo(this.config, { projectDrive: projectInfo.projectDrive, projectPath: projectInfo.projectPath }); return { enhanced: info.enabled, strategy: info.strategy, expectedPath: info.expectedPath, fallbackPath: info.fallbackPath, }; } } /** * 增强的上下文工厂创建函数 * 根据配置决定是否使用增强功能 */ export async function createEnhancedContextFactory(config) { // 如果启用了项目隔离,根据策略选择工厂 if (config.projectIsolation) { const strategy = config.projectIsolationSessionStrategy || 'system'; // 只有project策略使用原有逻辑,其他策略都使用增强工厂 if (strategy === 'project') { // 使用原有的上下文工厂(project策略) const { contextFactory } = await import('./browserContextFactory.js'); return contextFactory(config.browser); } // system和custom策略使用增强工厂 if (config.browser.remoteEndpoint) { // Remote endpoint 暂不支持增强功能,使用原有逻辑 const { contextFactory } = await import('./browserContextFactory.js'); return contextFactory(config.browser); } if (config.browser.cdpEndpoint) { // CDP endpoint 暂不支持增强功能,使用原有逻辑 const { contextFactory } = await import('./browserContextFactory.js'); return contextFactory(config.browser); } if (config.browser.isolated) { // Isolated mode 暂不支持增强功能,使用原有逻辑 const { contextFactory } = await import('./browserContextFactory.js'); return contextFactory(config.browser); } // 使用增强的持久化上下文工厂(system和custom策略) return new EnhancedPersistentContextFactory(config.browser, config); } // 未启用项目隔离,使用原有的上下文工厂 const { contextFactory } = await import('./browserContextFactory.js'); return contextFactory(config.browser); }