@ai-coding-labs/playwright-mcp-plus
Version:
Enhanced Playwright Tools for MCP with Project Session Isolation
265 lines (264 loc) • 10.2 kB
JavaScript
/**
* 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 'fs';
// import os from 'os';
import path from 'path';
import { z } from 'zod';
/**
* 项目隔离参数的Zod Schema
*/
export const projectIsolationSchema = z.object({
projectDrive: z.string().optional().describe('Project drive letter or root (e.g., "C:", "/") for session isolation'),
projectPath: z.string().optional().describe('Absolute path to project root directory for session isolation'),
});
/**
* 项目隔离参数的属性定义(用于避免使用.merge()导致的allOf问题)
*/
export const projectIsolationProperties = {
projectDrive: z.string().optional().describe('Project drive letter or root (e.g., "C:", "/") for session isolation'),
projectPath: z.string().optional().describe('Absolute path to project root directory for session isolation'),
};
/**
* 创建包含项目隔离参数的schema,避免使用.merge()导致的allOf问题
*/
export function createSchemaWithProjectIsolation(baseProperties) {
return z.object({
...baseProperties,
...projectIsolationProperties,
});
}
/**
* 验证项目隔离参数
*/
export function validateProjectIsolationParams(params) {
// 两个参数要么都提供,要么都不提供
return (!!params.projectDrive && !!params.projectPath) || (!params.projectDrive && !params.projectPath);
}
/**
* 验证项目隔离参数(考虑配置)
* 当项目隔离启用时,必须提供两个参数;否则参数可选
*/
export function validateProjectIsolationParamsWithConfig(params, projectIsolationEnabled) {
// 首先检查参数一致性
const paramsConsistent = validateProjectIsolationParams(params);
if (!paramsConsistent)
return false;
// 如果启用了项目隔离,必须提供两个参数
if (projectIsolationEnabled)
return !!(params.projectDrive && params.projectPath);
// 如果没有启用项目隔离,参数可选
return true;
}
/**
* 生成详细的项目隔离参数错误信息
*/
export function getProjectIsolationErrorMessage(projectIsolationEnabled) {
if (projectIsolationEnabled) {
return [
'Project isolation is enabled but required parameters are missing.',
'',
'Required parameters:',
'• projectDrive: Project drive letter or root directory',
'• projectPath: Absolute path to your project root directory',
'',
'Examples:',
'• Windows: projectDrive="C:", projectPath="C:\\Users\\username\\my-project"',
'• macOS/Linux: projectDrive="/", projectPath="/Users/username/my-project"',
'',
'How to obtain these values:',
'• projectDrive: The root of your file system (Windows: drive letter like "C:", Unix: "/")',
'• projectPath: The absolute path to your current project directory',
'• You can get the current directory path using: pwd (Unix) or cd (Windows)',
'',
'This ensures each project has isolated browser sessions and prevents data mixing between projects.'
].join('\n');
}
else {
return 'Both projectDrive and projectPath must be provided together, or neither should be provided.';
}
}
/**
* 项目隔离管理器
* 处理基于项目路径的用户数据目录创建和管理
*/
export class ProjectIsolationManager {
static SESSION_DIR_NAME = '.user-session-data-directory';
static GITIGNORE_COMMENT = '# Playwright MCP session data (auto-generated)';
static MAX_PATH_LENGTH_WINDOWS = 260;
/**
* 根据项目信息创建用户数据目录路径
*/
static createUserDataDir(projectInfo) {
if (!projectInfo.projectPath || !projectInfo.projectDrive)
return undefined;
try {
// 验证项目路径
if (!this.validateProjectPath(projectInfo.projectPath))
return undefined;
// 规范化路径
const normalizedPath = path.resolve(projectInfo.projectPath);
// 创建会话数据目录路径
const sessionDataDir = path.join(normalizedPath, this.SESSION_DIR_NAME);
// Windows路径长度检查
if (process.platform === 'win32' && sessionDataDir.length > this.MAX_PATH_LENGTH_WINDOWS) {
}
return sessionDataDir;
}
catch (error) {
return undefined;
}
}
/**
* 确保项目数据目录存在
*/
static async ensureProjectDataDir(userDataDir) {
try {
// 检查父目录是否存在且可写
const parentDir = path.dirname(userDataDir);
await this.checkDirectoryPermissions(parentDir);
// 创建会话数据目录
await fs.promises.mkdir(userDataDir, { recursive: true });
// 检查.gitignore并提示用户
await this.checkAndPromptGitignore(parentDir);
}
catch (error) {
if (error.code === 'EACCES' || error.code === 'EPERM')
throw new Error(`Permission denied: Cannot create session directory at ${userDataDir}. Please check directory permissions.`);
else if (error.code === 'ENOSPC')
throw new Error(`No space left on device: Cannot create session directory at ${userDataDir}.`);
else
throw new Error(`Failed to create session directory: ${error.message}`);
}
}
/**
* 验证项目路径的有效性
*/
static validateProjectPath(projectPath) {
if (!projectPath || typeof projectPath !== 'string')
return false;
try {
// 基本路径验证
const normalizedPath = path.resolve(projectPath);
// 检查路径是否为绝对路径
if (!path.isAbsolute(normalizedPath))
return false;
// 安全检查:防止路径遍历攻击
if (normalizedPath.includes('..') || normalizedPath.includes('./'))
return false;
// 检查路径是否存在
try {
const stats = fs.statSync(normalizedPath);
return stats.isDirectory();
}
catch {
// 目录不存在也是有效的(可能会创建)
return true;
}
}
catch {
return false;
}
}
/**
* 生成 .gitignore 提示信息
*/
static getGitignoreHint() {
return [
'📌 Playwright MCP Notice:',
` Session data directory created at: ${this.SESSION_DIR_NAME}/`,
' Please add the following line to your .gitignore file:',
'',
` ${this.SESSION_DIR_NAME}/`,
'',
' This will prevent browser session data from being committed to your repository.',
].join('\n');
}
/**
* 检查目录权限
*/
static async checkDirectoryPermissions(dirPath) {
try {
await fs.promises.access(dirPath, fs.constants.F_OK | fs.constants.W_OK);
}
catch (error) {
if (error.code === 'ENOENT')
throw new Error(`Directory does not exist: ${dirPath}`);
else if (error.code === 'EACCES')
throw new Error(`Permission denied: Cannot write to directory ${dirPath}`);
else
throw error;
}
}
/**
* 检查并提示.gitignore配置
*/
static async checkAndPromptGitignore(projectDir) {
try {
const gitignorePath = path.join(projectDir, '.gitignore');
// 检查.gitignore文件是否存在
let gitignoreExists = false;
let gitignoreContent = '';
try {
gitignoreContent = await fs.promises.readFile(gitignorePath, 'utf-8');
gitignoreExists = true;
}
catch {
// .gitignore文件不存在
}
// 检查是否已经包含忽略规则
const hasIgnoreRule = gitignoreContent.includes(this.SESSION_DIR_NAME);
if (!hasIgnoreRule) {
// 输出提示信息
// 可选:自动添加到.gitignore(如果文件存在)
if (gitignoreExists) {
try {
const newContent = gitignoreContent +
(gitignoreContent.endsWith('\n') ? '' : '\n') +
'\n' + this.GITIGNORE_COMMENT + '\n' +
this.SESSION_DIR_NAME + '/\n';
await fs.promises.writeFile(gitignorePath, newContent);
}
catch (error) {
}
}
}
}
catch (error) {
// 忽略.gitignore检查错误,不影响主要功能
}
}
/**
* 清理工具:列出所有项目会话目录
*/
static async listProjectSessionDirs() {
// 这个功能可以在后续版本中实现
// 用于帮助用户清理不再使用的项目会话数据
return [];
}
/**
* 清理工具:删除指定项目的会话数据
*/
static async cleanupProjectSessionDir(projectPath) {
try {
const sessionDir = path.join(projectPath, this.SESSION_DIR_NAME);
await fs.promises.rm(sessionDir, { recursive: true, force: true });
return true;
}
catch (error) {
return false;
}
}
}