UNPKG

mcp-chrome-bridge

Version:

Chrome Native-Messaging host (Node)

443 lines 21.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.writeFile = exports.mkdir = exports.access = void 0; exports.getLogDir = getLogDir; exports.colorText = colorText; exports.getUserManifestPath = getUserManifestPath; exports.getSystemManifestPath = getSystemManifestPath; exports.getMainPath = getMainPath; exports.writeNodePathFile = writeNodePathFile; exports.ensureExecutionPermissions = ensureExecutionPermissions; exports.createManifestContent = createManifestContent; exports.registerUserLevelHostWithNodePath = registerUserLevelHostWithNodePath; exports.tryRegisterUserLevelHost = tryRegisterUserLevelHost; exports.registerWithElevatedPermissions = registerWithElevatedPermissions; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const child_process_1 = require("child_process"); const util_1 = require("util"); const constant_1 = require("./constant"); const browser_config_1 = require("./browser-config"); exports.access = (0, util_1.promisify)(fs_1.default.access); exports.mkdir = (0, util_1.promisify)(fs_1.default.mkdir); exports.writeFile = (0, util_1.promisify)(fs_1.default.writeFile); /** * Get the log directory path for wrapper scripts. * Uses platform-appropriate user directories to avoid permission issues. * * - macOS: ~/Library/Logs/mcp-chrome-bridge * - Windows: %LOCALAPPDATA%/mcp-chrome-bridge/logs * - Linux: $XDG_STATE_HOME/mcp-chrome-bridge/logs or ~/.local/state/mcp-chrome-bridge/logs */ function getLogDir() { const homedir = os_1.default.homedir(); if (os_1.default.platform() === 'darwin') { return path_1.default.join(homedir, 'Library', 'Logs', 'mcp-chrome-bridge'); } else if (os_1.default.platform() === 'win32') { return path_1.default.join(process.env.LOCALAPPDATA || path_1.default.join(homedir, 'AppData', 'Local'), 'mcp-chrome-bridge', 'logs'); } else { // Linux: XDG_STATE_HOME or ~/.local/state const xdgState = process.env.XDG_STATE_HOME || path_1.default.join(homedir, '.local', 'state'); return path_1.default.join(xdgState, 'mcp-chrome-bridge', 'logs'); } } /** * 打印彩色文本 */ function colorText(text, color) { const colors = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', reset: '\x1b[0m', }; return colors[color] + text + colors.reset; } /** * Get user-level manifest file path */ function getUserManifestPath() { if (os_1.default.platform() === 'win32') { // Windows: %APPDATA%\Google\Chrome\NativeMessagingHosts\ return path_1.default.join(process.env.APPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Roaming'), 'Google', 'Chrome', 'NativeMessagingHosts', `${constant_1.HOST_NAME}.json`); } else if (os_1.default.platform() === 'darwin') { // macOS: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/ return path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts', `${constant_1.HOST_NAME}.json`); } else { // Linux: ~/.config/google-chrome/NativeMessagingHosts/ return path_1.default.join(os_1.default.homedir(), '.config', 'google-chrome', 'NativeMessagingHosts', `${constant_1.HOST_NAME}.json`); } } /** * Get system-level manifest file path */ function getSystemManifestPath() { if (os_1.default.platform() === 'win32') { // Windows: %ProgramFiles%\Google\Chrome\NativeMessagingHosts\ return path_1.default.join(process.env.ProgramFiles || 'C:\\Program Files', 'Google', 'Chrome', 'NativeMessagingHosts', `${constant_1.HOST_NAME}.json`); } else if (os_1.default.platform() === 'darwin') { // macOS: /Library/Google/Chrome/NativeMessagingHosts/ return path_1.default.join('/Library', 'Google', 'Chrome', 'NativeMessagingHosts', `${constant_1.HOST_NAME}.json`); } else { // Linux: /etc/opt/chrome/native-messaging-hosts/ return path_1.default.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${constant_1.HOST_NAME}.json`); } } /** * Get native host startup script file path */ async function getMainPath() { try { const packageDistDir = path_1.default.join(__dirname, '..'); const wrapperScriptName = process.platform === 'win32' ? 'run_host.bat' : 'run_host.sh'; const absoluteWrapperPath = path_1.default.resolve(packageDistDir, wrapperScriptName); return absoluteWrapperPath; } catch (error) { console.log(colorText('Cannot find global package path, using current directory', 'yellow')); throw error; } } /** * Write Node.js executable path to node_path.txt for run_host scripts. * This ensures the native host uses the same Node.js version that was used during installation, * avoiding NODE_MODULE_VERSION mismatch errors with native modules like better-sqlite3. * * @param distDir - The dist directory where node_path.txt should be written * @param nodeExecPath - The Node.js executable path to write (defaults to current process.execPath) */ function writeNodePathFile(distDir, nodeExecPath = process.execPath) { try { const nodePathFile = path_1.default.join(distDir, 'node_path.txt'); fs_1.default.mkdirSync(distDir, { recursive: true }); console.log(colorText(`Writing Node.js path: ${nodeExecPath}`, 'blue')); fs_1.default.writeFileSync(nodePathFile, nodeExecPath, 'utf8'); console.log(colorText('✓ Node.js path written for run_host scripts', 'green')); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.warn(colorText(`⚠️ Failed to write Node.js path: ${message}`, 'yellow')); } } /** * 确保关键文件具有执行权限 */ async function ensureExecutionPermissions() { try { const packageDistDir = path_1.default.join(__dirname, '..'); if (process.platform === 'win32') { // Windows 平台处理 await ensureWindowsFilePermissions(packageDistDir); return; } // Unix/Linux 平台处理 const filesToCheck = [ path_1.default.join(packageDistDir, 'index.js'), path_1.default.join(packageDistDir, 'run_host.sh'), path_1.default.join(packageDistDir, 'cli.js'), ]; for (const filePath of filesToCheck) { if (fs_1.default.existsSync(filePath)) { try { fs_1.default.chmodSync(filePath, '755'); console.log(colorText(`✓ Set execution permissions for ${path_1.default.basename(filePath)}`, 'green')); } catch (err) { console.warn(colorText(`⚠️ Unable to set execution permissions for ${path_1.default.basename(filePath)}: ${err.message}`, 'yellow')); } } else { console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow')); } } } catch (error) { console.warn(colorText(`⚠️ Error ensuring execution permissions: ${error.message}`, 'yellow')); } } /** * Windows 平台文件权限处理 */ async function ensureWindowsFilePermissions(packageDistDir) { const filesToCheck = [ path_1.default.join(packageDistDir, 'index.js'), path_1.default.join(packageDistDir, 'run_host.bat'), path_1.default.join(packageDistDir, 'cli.js'), ]; for (const filePath of filesToCheck) { if (fs_1.default.existsSync(filePath)) { try { // 检查文件是否为只读,如果是则移除只读属性 const stats = fs_1.default.statSync(filePath); if (!(stats.mode & parseInt('200', 8))) { // 检查写权限 // 尝试移除只读属性 fs_1.default.chmodSync(filePath, stats.mode | parseInt('200', 8)); console.log(colorText(`✓ Removed read-only attribute from ${path_1.default.basename(filePath)}`, 'green')); } // 验证文件可读性 fs_1.default.accessSync(filePath, fs_1.default.constants.R_OK); console.log(colorText(`✓ Verified file accessibility for ${path_1.default.basename(filePath)}`, 'green')); } catch (err) { console.warn(colorText(`⚠️ Unable to verify file permissions for ${path_1.default.basename(filePath)}: ${err.message}`, 'yellow')); } } else { console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow')); } } } /** * Create Native Messaging host manifest content */ async function createManifestContent() { const mainPath = await getMainPath(); return { name: constant_1.HOST_NAME, description: constant_1.DESCRIPTION, path: mainPath, // Node.js可执行文件路径 type: 'stdio', allowed_origins: [`chrome-extension://${constant_1.EXTENSION_ID}/`], }; } /** * 验证Windows注册表项是否存在且指向正确路径 */ function verifyWindowsRegistryEntry(registryKey, expectedPath) { if (os_1.default.platform() !== 'win32') { return true; // 非Windows平台跳过验证 } const normalizeForCompare = (filePath) => path_1.default.normalize(filePath).toLowerCase(); try { const output = (0, child_process_1.execSync)(`reg query "${registryKey}" /ve`, { encoding: 'utf8', stdio: 'pipe', }); const lines = output .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); for (const line of lines) { const match = line.match(/REG_SZ\s+(.*)$/i); if (!(match === null || match === void 0 ? void 0 : match[1])) continue; const actualPath = match[1].trim(); return normalizeForCompare(actualPath) === normalizeForCompare(expectedPath); } } catch (_a) { // ignore } return false; } /** * Write node_path.txt and then register user-level Native Messaging host. * This is the recommended entry point for development and production registration, * as it ensures the Node.js path is captured before registration. * * @param browsers - Optional list of browsers to register for * @returns true if at least one browser was registered successfully */ async function registerUserLevelHostWithNodePath(browsers) { writeNodePathFile(path_1.default.join(__dirname, '..')); return tryRegisterUserLevelHost(browsers); } /** * 尝试注册用户级别的Native Messaging主机 */ async function tryRegisterUserLevelHost(targetBrowsers) { try { console.log(colorText('Attempting to register user-level Native Messaging host...', 'blue')); // 1. 确保执行权限 await ensureExecutionPermissions(); // 2. 确定要注册的浏览器 const browsersToRegister = targetBrowsers || (0, browser_config_1.detectInstalledBrowsers)(); if (browsersToRegister.length === 0) { // 如果没有检测到浏览器,默认注册Chrome和Chromium browsersToRegister.push(browser_config_1.BrowserType.CHROME, browser_config_1.BrowserType.CHROMIUM); console.log(colorText('No browsers detected, registering for Chrome and Chromium by default', 'yellow')); } else { console.log(colorText(`Detected browsers: ${browsersToRegister.join(', ')}`, 'blue')); } // 3. 创建清单内容 const manifest = await createManifestContent(); let successCount = 0; const results = []; // 4. 为每个浏览器注册 for (const browserType of browsersToRegister) { const config = (0, browser_config_1.getBrowserConfig)(browserType); console.log(colorText(`\nRegistering for ${config.displayName}...`, 'blue')); try { // 确保目录存在 await (0, exports.mkdir)(path_1.default.dirname(config.userManifestPath), { recursive: true }); // 写入清单文件 await (0, exports.writeFile)(config.userManifestPath, JSON.stringify(manifest, null, 2)); console.log(colorText(`✓ Manifest written to ${config.userManifestPath}`, 'green')); // Windows需要额外注册表项 if (os_1.default.platform() === 'win32' && config.registryKey) { try { // 注意:不需要手动双写反斜杠,reg 命令会正确处理 Windows 路径 const regCommand = `reg add "${config.registryKey}" /ve /t REG_SZ /d "${config.userManifestPath}" /f`; (0, child_process_1.execSync)(regCommand, { stdio: 'pipe' }); if (verifyWindowsRegistryEntry(config.registryKey, config.userManifestPath)) { console.log(colorText(`✓ Registry entry created for ${config.displayName}`, 'green')); } else { throw new Error('Registry verification failed'); } } catch (error) { throw new Error(`Registry error: ${error.message}`); } } successCount++; results.push({ browser: config.displayName, success: true }); console.log(colorText(`✓ Successfully registered ${config.displayName}`, 'green')); } catch (error) { results.push({ browser: config.displayName, success: false, error: error.message }); console.log(colorText(`✗ Failed to register ${config.displayName}: ${error.message}`, 'red')); } } // 5. 报告结果 console.log(colorText('\n===== Registration Summary =====', 'blue')); for (const result of results) { if (result.success) { console.log(colorText(`✓ ${result.browser}: Success`, 'green')); } else { console.log(colorText(`✗ ${result.browser}: Failed - ${result.error}`, 'red')); } } return successCount > 0; } catch (error) { console.log(colorText(`User-level registration failed: ${error instanceof Error ? error.message : String(error)}`, 'yellow')); return false; } } // 导入is-admin包(仅在Windows平台使用) let isAdmin = () => false; if (process.platform === 'win32') { try { isAdmin = require('is-admin'); } catch (error) { console.warn('缺少is-admin依赖,Windows平台下可能无法正确检测管理员权限'); console.warn(error); } } /** * 使用提升权限注册系统级清单 */ async function registerWithElevatedPermissions() { try { console.log(colorText('Attempting to register system-level manifest...', 'blue')); // 1. 确保执行权限 await ensureExecutionPermissions(); // 2. 准备清单内容 const manifest = await createManifestContent(); // 3. 获取系统级清单路径 const manifestPath = getSystemManifestPath(); // 4. 创建临时清单文件 const tempManifestPath = path_1.default.join(os_1.default.tmpdir(), `${constant_1.HOST_NAME}.json`); await (0, exports.writeFile)(tempManifestPath, JSON.stringify(manifest, null, 2)); // 5. 检测是否已经有管理员权限 const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac const hasAdminRights = process.platform === 'win32' ? isAdmin() : false; // Windows平台检测管理员权限 const hasElevatedPermissions = isRoot || hasAdminRights; // 准备命令 const command = os_1.default.platform() === 'win32' ? `if not exist "${path_1.default.dirname(manifestPath)}" mkdir "${path_1.default.dirname(manifestPath)}" && copy "${tempManifestPath}" "${manifestPath}"` : `mkdir -p "${path_1.default.dirname(manifestPath)}" && cp "${tempManifestPath}" "${manifestPath}" && chmod 644 "${manifestPath}"`; if (hasElevatedPermissions) { // 已经有管理员权限,直接执行命令 try { // 创建目录 if (!fs_1.default.existsSync(path_1.default.dirname(manifestPath))) { fs_1.default.mkdirSync(path_1.default.dirname(manifestPath), { recursive: true }); } // 复制文件 fs_1.default.copyFileSync(tempManifestPath, manifestPath); // 设置权限(非Windows平台) if (os_1.default.platform() !== 'win32') { fs_1.default.chmodSync(manifestPath, '644'); } console.log(colorText('System-level manifest registration successful!', 'green')); } catch (error) { console.error(colorText(`System-level manifest installation failed: ${error.message}`, 'red')); throw error; } } else { // 没有管理员权限,打印手动操作提示 console.log(colorText('⚠️ Administrator privileges required for system-level installation', 'yellow')); console.log(colorText('Please run one of the following commands with administrator privileges:', 'blue')); if (os_1.default.platform() === 'win32') { console.log(colorText(' 1. Open Command Prompt as Administrator and run:', 'blue')); console.log(colorText(` ${command}`, 'cyan')); } else { console.log(colorText(' 1. Run with sudo:', 'blue')); console.log(colorText(` sudo ${command}`, 'cyan')); } console.log(colorText(' 2. Or run the registration command with elevated privileges:', 'blue')); console.log(colorText(` sudo ${constant_1.COMMAND_NAME} register --system`, 'cyan')); throw new Error('Administrator privileges required for system-level installation'); } // 6. Windows特殊处理 - 设置系统级注册表 if (os_1.default.platform() === 'win32') { const registryKey = `HKLM\\Software\\Google\\Chrome\\NativeMessagingHosts\\${constant_1.HOST_NAME}`; // 注意:不需要手动双写反斜杠,reg 命令会正确处理 Windows 路径 const regCommand = `reg add "${registryKey}" /ve /t REG_SZ /d "${manifestPath}" /f`; console.log(colorText(`Creating system registry entry: ${registryKey}`, 'blue')); console.log(colorText(`Manifest path: ${manifestPath}`, 'blue')); if (hasElevatedPermissions) { // 已经有管理员权限,直接执行注册表命令 try { (0, child_process_1.execSync)(regCommand, { stdio: 'pipe' }); // 验证注册表项是否创建成功 if (verifyWindowsRegistryEntry(registryKey, manifestPath)) { console.log(colorText('Windows registry entry created successfully!', 'green')); } else { console.log(colorText('⚠️ Registry entry created but verification failed', 'yellow')); } } catch (error) { console.error(colorText(`Windows registry entry creation failed: ${error.message}`, 'red')); console.error(colorText(`Command: ${regCommand}`, 'red')); throw error; } } else { // 没有管理员权限,打印手动操作提示 console.log(colorText('⚠️ Administrator privileges required for Windows registry modification', 'yellow')); console.log(colorText('Please run the following command as Administrator:', 'blue')); console.log(colorText(` ${regCommand}`, 'cyan')); console.log(colorText('Or run the registration command with elevated privileges:', 'blue')); console.log(colorText(` Run Command Prompt as Administrator and execute: ${constant_1.COMMAND_NAME} register --system`, 'cyan')); throw new Error('Administrator privileges required for Windows registry modification'); } } } catch (error) { console.error(colorText(`注册失败: ${error.message}`, 'red')); throw error; } } //# sourceMappingURL=utils.js.map