Spaces:
Paused
Paused
| // auto_connect_aistudio.js (v2.9 - Refined Launch & Page Handling + Beautified Output) | |
| const { spawn, execSync } = require('child_process'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| const readline = require('readline'); | |
| // --- Configuration --- | |
| const DEBUGGING_PORT = 8848; | |
| const TARGET_URL = 'https://aistudio.google.com/prompts/new_chat'; // Target page | |
| const SERVER_SCRIPT_FILENAME = 'server.cjs'; // Corrected script name | |
| const CONNECTION_RETRIES = 5; | |
| const RETRY_DELAY_MS = 4000; | |
| const CONNECT_TIMEOUT_MS = 20000; // Timeout for connecting to CDP | |
| const NAVIGATION_TIMEOUT_MS = 35000; // Increased timeout for page navigation | |
| const CDP_ADDRESS = `http://127.0.0.1:${DEBUGGING_PORT}`; | |
| // --- ANSI Colors --- | |
| const RESET = '\x1b[0m'; | |
| const BRIGHT = '\x1b[1m'; | |
| const DIM = '\x1b[2m'; | |
| const RED = '\x1b[31m'; | |
| const GREEN = '\x1b[32m'; | |
| const YELLOW = '\x1b[33m'; | |
| const BLUE = '\x1b[34m'; | |
| const MAGENTA = '\x1b[35m'; | |
| const CYAN = '\x1b[36m'; | |
| // --- Globals --- | |
| const SERVER_SCRIPT_PATH = path.join(__dirname, SERVER_SCRIPT_FILENAME); | |
| let playwright; // Loaded in checkDependencies | |
| // --- Platform-Specific Chrome Path --- | |
| function getChromePath() { | |
| switch (process.platform) { | |
| case 'darwin': | |
| return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; | |
| case 'win32': | |
| // 尝试 Program Files 和 Program Files (x86) | |
| const winPaths = [ | |
| path.join(process.env.ProgramFiles || '', 'Google\Chrome\Application\chrome.exe'), | |
| path.join(process.env['ProgramFiles(x86)'] || '', 'Google\Chrome\Application\chrome.exe') | |
| ]; | |
| return winPaths.find(p => fs.existsSync(p)); | |
| case 'linux': | |
| // 尝试常见的 Linux 路径 | |
| const linuxPaths = [ | |
| '/usr/bin/google-chrome', | |
| '/usr/bin/google-chrome-stable', | |
| '/opt/google/chrome/chrome', | |
| // Add path for Flatpak installation if needed | |
| // '/var/lib/flatpak/exports/bin/com.google.Chrome' | |
| ]; | |
| return linuxPaths.find(p => fs.existsSync(p)); | |
| default: | |
| return null; // 不支持的平台 | |
| } | |
| } | |
| const chromeExecutablePath = getChromePath(); | |
| // --- 端口检查函数 --- | |
| function isPortInUse(port) { | |
| const platform = process.platform; | |
| let command; | |
| // console.log(`${DIM} 检查端口 ${port}...${RESET}`); // Optional: Verbose check | |
| try { | |
| if (platform === 'win32') { | |
| // 在 Windows 上,查找监听状态的 TCP 端口 | |
| command = `netstat -ano | findstr LISTENING | findstr :${port}`; | |
| execSync(command); // 如果找到,不会抛出错误 | |
| return true; | |
| } else if (platform === 'darwin' || platform === 'linux') { | |
| // 在 macOS 或 Linux 上,查找监听该端口的进程 | |
| command = `lsof -i tcp:${port} -sTCP:LISTEN`; | |
| execSync(command); // 如果找到,不会抛出错误 | |
| return true; | |
| } | |
| } catch (error) { | |
| // 如果命令执行失败(通常意味着找不到匹配的进程),则端口未被占用 | |
| // console.log(`端口 ${port} 检查命令执行失败或未找到进程:`, error.message.split('\n')[0]); // 可选的调试信息 | |
| return false; | |
| } | |
| // 对于不支持的平台,保守地假设端口未被占用 | |
| return false; | |
| } | |
| // --- 查找占用端口的 PID --- (新增) | |
| function findPidsUsingPort(port) { | |
| const platform = process.platform; | |
| const pids = []; | |
| let command; | |
| try { | |
| console.log(`${DIM} 正在查找占用端口 ${port} 的进程...${RESET}`); | |
| if (platform === 'win32') { | |
| command = `netstat -ano | findstr LISTENING | findstr :${port}`; | |
| const output = execSync(command).toString(); | |
| const lines = output.trim().split('\n'); | |
| for (const line of lines) { | |
| const parts = line.trim().split(/\s+/); | |
| const pid = parts[parts.length - 1]; // PID is the last column | |
| if (pid && !isNaN(pid)) { | |
| pids.push(pid); | |
| } | |
| } | |
| } else { // macOS or Linux | |
| command = `lsof -t -i tcp:${port} -sTCP:LISTEN`; | |
| const output = execSync(command).toString(); | |
| const lines = output.trim().split('\n'); | |
| for (const line of lines) { | |
| const pid = line.trim(); | |
| if (pid && !isNaN(pid)) { | |
| pids.push(pid); | |
| } | |
| } | |
| } | |
| if (pids.length > 0) { | |
| console.log(` ${YELLOW}找到占用端口 ${port} 的 PID: ${pids.join(', ')}${RESET}`); | |
| } else { | |
| console.log(` ${GREEN}未找到明确监听端口 ${port} 的进程。${RESET}`); | |
| } | |
| } catch (error) { | |
| // 命令失败通常意味着没有找到进程 | |
| console.log(` ${GREEN}查找端口 ${port} 进程的命令执行失败或无结果。${RESET}`); | |
| } | |
| return [...new Set(pids)]; // 返回去重后的 PID 列表 | |
| } | |
| // --- 结束进程 --- (新增) | |
| function killProcesses(pids) { | |
| if (pids.length === 0) return true; // 没有进程需要结束 | |
| const platform = process.platform; | |
| let success = true; | |
| console.log(`${YELLOW} 正在尝试结束 PID: ${pids.join(', ')}...${RESET}`); | |
| for (const pid of pids) { | |
| try { | |
| if (platform === 'win32') { | |
| execSync(`taskkill /F /PID ${pid}`); | |
| console.log(` ${GREEN}✅ 成功结束 PID ${pid} (Windows)${RESET}`); | |
| } else { // macOS or Linux | |
| execSync(`kill -9 ${pid}`); | |
| console.log(` ${GREEN}✅ 成功结束 PID ${pid} (macOS/Linux)${RESET}`); | |
| } | |
| } catch (error) { | |
| console.warn(` ${RED}⚠️ 结束 PID ${pid} 时出错: ${error.message.split('\n')[0]}${RESET}`); | |
| // 可能原因:进程已不存在、权限不足等 | |
| success = false; // 标记至少有一个失败了 | |
| } | |
| } | |
| return success; | |
| } | |
| // --- 创建 Readline Interface --- | |
| function askQuestion(query) { | |
| const rl = readline.createInterface({ | |
| input: process.stdin, | |
| output: process.stdout, | |
| }); | |
| return new Promise(resolve => rl.question(query, ans => { | |
| rl.close(); | |
| resolve(ans); | |
| })) | |
| } | |
| // --- 步骤 1: 检查 Playwright 依赖 --- | |
| async function checkDependencies() { | |
| console.log(`${CYAN}-------------------------------------------------${RESET}`); | |
| console.log(`${CYAN}--- 步骤 1: 检查依赖项 ---${RESET}`); | |
| console.log('将检查以下模块是否已安装:'); | |
| const requiredModules = ['express', 'playwright', '@playwright/test', 'cors']; | |
| const missingModules = []; | |
| let allFound = true; | |
| for (const moduleName of requiredModules) { | |
| process.stdout.write(` - ${moduleName} ... `); | |
| try { | |
| require.resolve(moduleName); // Use require.resolve for checking existence without loading | |
| console.log(`${GREEN}✓ 已找到${RESET}`); // Green checkmark | |
| } catch (error) { | |
| if (error.code === 'MODULE_NOT_FOUND') { | |
| console.log(`${RED}❌ 未找到${RESET}`); // Red X | |
| missingModules.push(moduleName); | |
| allFound = false; | |
| } else { | |
| console.log(`${RED}❌ 检查时出错: ${error.message}${RESET}`); | |
| allFound = false; | |
| // Consider exiting if it's not MODULE_NOT_FOUND? | |
| // return false; | |
| } | |
| } | |
| } | |
| process.stdout.write(` - 服务器脚本 (${SERVER_SCRIPT_FILENAME}) ... `); | |
| if (!fs.existsSync(SERVER_SCRIPT_PATH)) { | |
| console.log(`${RED}❌ 未找到${RESET}`); // Red X | |
| console.error(` ${RED}错误: 未在预期路径找到 '${SERVER_SCRIPT_FILENAME}' 文件。${RESET}`); | |
| console.error(` 预期路径: ${SERVER_SCRIPT_PATH}`); | |
| console.error(` 请确保 '${SERVER_SCRIPT_FILENAME}' 与此脚本位于同一目录。`); | |
| allFound = false; | |
| } else { | |
| console.log(`${GREEN}✓ 已找到${RESET}`); // Green checkmark | |
| } | |
| if (!allFound) { | |
| console.log(`\n${RED}-------------------------------------------------${RESET}`); | |
| console.error(`${RED}❌ 错误: 依赖项检查未通过!${RESET}`); | |
| if (missingModules.length > 0) { | |
| console.error(` ${RED}缺少以下 Node.js 模块: ${missingModules.join(', ')}${RESET}`); | |
| console.log(' 请根据您使用的包管理器运行以下命令安装依赖:'); | |
| console.log(` ${MAGENTA}npm install ${missingModules.join(' ')}${RESET}`); | |
| console.log(' 或'); | |
| console.log(` ${MAGENTA}yarn add ${missingModules.join(' ')}${RESET}`); | |
| console.log(' 或'); | |
| console.log(` ${MAGENTA}pnpm install ${missingModules.join(' ')}${RESET}`); | |
| console.log(' (如果已安装但仍提示未找到,请尝试删除 node_modules 目录和 package-lock.json/yarn.lock 文件后重新安装)'); | |
| } | |
| if (!fs.existsSync(SERVER_SCRIPT_PATH)) { | |
| console.error(` ${RED}缺少必要的服务器脚本文件: ${SERVER_SCRIPT_FILENAME}${RESET}`); | |
| console.error(` 请确保它和 auto_connect_aistudio.cjs 在同一个文件夹内。`); | |
| } | |
| console.log(`${RED}-------------------------------------------------${RESET}`); | |
| return false; | |
| } | |
| console.log(`\n${GREEN}✅ 所有依赖检查通过。${RESET}`); | |
| playwright = require('playwright'); // Load playwright only after checks | |
| return true; | |
| } | |
| // --- 步骤 2: 检查并启动 Chrome --- | |
| async function launchChrome() { | |
| console.log(`${CYAN}-------------------------------------------------${RESET}`); | |
| console.log(`${CYAN}--- 步骤 2: 启动或连接 Chrome (调试端口 ${DEBUGGING_PORT}) ---${RESET}`); | |
| // 首先检查端口是否被占用 | |
| if (isPortInUse(DEBUGGING_PORT)) { | |
| console.log(`${YELLOW}⚠️ 警告: 端口 ${DEBUGGING_PORT} 已被占用。${RESET}`); | |
| console.log(' 这通常意味着已经有一个 Chrome 实例在监听此端口。'); | |
| const question = `选择操作: [Y/n] | |
| ${GREEN}Y (默认): 尝试连接现有 Chrome 实例并启动 API 服务器。${RESET} | |
| ${YELLOW}n: 自动强行结束占用端口 ${DEBUGGING_PORT} 的进程,然后启动新的 Chrome 实例。${RESET} | |
| 请输入选项 [Y/n]: `; | |
| const answer = await askQuestion(question); | |
| if (answer.toLowerCase() === 'n') { | |
| console.log(`\n好的,您选择了启动新实例。将尝试自动清理端口...`); | |
| const pids = findPidsUsingPort(DEBUGGING_PORT); | |
| if (pids.length > 0) { | |
| const killSuccess = killProcesses(pids); | |
| if (killSuccess) { | |
| console.log(` ${GREEN}✅ 尝试结束进程完成。等待 1 秒检查端口...${RESET}`); | |
| await new Promise(resolve => setTimeout(resolve, 1000)); // 短暂等待 | |
| if (isPortInUse(DEBUGGING_PORT)) { | |
| console.error(`${RED}❌ 错误: 尝试结束后,端口 ${DEBUGGING_PORT} 仍然被占用。${RESET}`); | |
| console.error(' 可能原因:权限不足,或进程未能正常终止。请尝试手动结束进程。' ); | |
| // 提供手动清理提示 | |
| console.log(`${YELLOW}提示: 您可以使用以下命令查找进程 ID (PID):${RESET}`); | |
| if (process.platform === 'win32') { | |
| console.log(` - 在 CMD 或 PowerShell 中: netstat -ano | findstr :${DEBUGGING_PORT}`); | |
| console.log(' - 找到 PID 后,使用: taskkill /F /PID <PID>'); | |
| } else { // macOS or Linux | |
| console.log(` - 在终端中: lsof -t -i:${DEBUGGING_PORT}`); | |
| console.log(' - 找到 PID 后,使用: kill -9 <PID>'); | |
| } | |
| await askQuestion('请在手动结束进程后,按 Enter 键重试脚本...'); | |
| process.exit(1); // 退出,让用户处理后重跑 | |
| } else { | |
| console.log(` ${GREEN}✅ 端口 ${DEBUGGING_PORT} 现在空闲。${RESET}`); | |
| // 端口已清理,继续执行下面的 Chrome 启动流程 | |
| } | |
| } else { | |
| console.error(`${RED}❌ 错误: 尝试结束部分或全部占用端口的进程失败。${RESET}`); | |
| console.error(' 请检查日志中的具体错误信息,可能需要手动结束进程。'); | |
| await askQuestion('请在手动结束进程后,按 Enter 键重试脚本...'); | |
| process.exit(1); // 退出,让用户处理后重跑 | |
| } | |
| } else { | |
| console.log(`${YELLOW} 虽然端口被占用,但未能找到具体监听的进程 PID。可能情况复杂,建议手动检查。${RESET}` ); | |
| await askQuestion('请手动检查并确保端口空闲后,按 Enter 键重试脚本...'); | |
| process.exit(1); // 退出 | |
| } | |
| // 如果代码执行到这里,意味着端口清理成功,将继续启动 Chrome | |
| console.log(`\n准备启动新的 Chrome 实例...`); | |
| } else { | |
| console.log(`\n好的,将尝试连接到现有的 Chrome 实例...`); | |
| return 'use_existing'; // 特殊返回值,告知主流程跳过启动,直接连接 | |
| } | |
| } | |
| // --- 如果端口未被占用,或者用户选择 'n' 且自动清理成功 --- | |
| if (!chromeExecutablePath) { | |
| console.error(`${RED}❌ 错误: 未能在当前操作系统 (${process.platform}) 的常见路径找到 Chrome 可执行文件。${RESET}`); | |
| console.error(' 请确保已安装 Google Chrome,或修改脚本中的 getChromePath 函数以指向正确的路径。'); | |
| if (process.platform === 'win32') { | |
| console.error(' (已尝试查找 %ProgramFiles% 和 %ProgramFiles(x86)% 下的路径)'); | |
| } else if (process.platform === 'linux') { | |
| console.error(' (已尝试查找 /usr/bin/google-chrome, /usr/bin/google-chrome-stable, /opt/google/chrome/chrome)'); | |
| } | |
| return false; | |
| } | |
| console.log(` ${GREEN}找到 Chrome 路径:${RESET} ${chromeExecutablePath}`); | |
| // 只有在明确需要启动新实例时才提示关闭其他实例 | |
| // (如果上面选择了 'n' 并清理成功,这里 isPortInUse 应该返回 false) | |
| if (!isPortInUse(DEBUGGING_PORT)) { | |
| console.log(`${YELLOW}⚠️ 重要提示:为了确保新的调试端口生效,建议先手动完全退出所有*其他*可能干扰的 Google Chrome 实例。${RESET}`); | |
| console.log(' (在 macOS 上通常是 Cmd+Q,Windows/Linux 上是关闭所有窗口)'); | |
| await askQuestion('请确认已处理好其他 Chrome 实例,然后按 Enter 键继续启动...'); | |
| } else { | |
| // 理论上不应该到这里,因为端口已被清理或选择了 use_existing | |
| console.warn(` ${YELLOW}警告:端口 ${DEBUGGING_PORT} 意外地仍被占用。继续尝试启动,但这极有可能失败。${RESET}`); | |
| await askQuestion('请按 Enter 键继续尝试启动...'); | |
| } | |
| console.log(`正在尝试启动 Chrome...`); | |
| console.log(` 路径: "${chromeExecutablePath}"`); | |
| // --- 修改:添加启动参数 --- | |
| const chromeArgs = [ | |
| `--remote-debugging-port=${DEBUGGING_PORT}`, | |
| `--window-size=460,800` // 指定宽度为 460px,高度暂定为 800px (可以根据需要调整) | |
| // 你可以在这里添加其他需要的 Chrome 启动参数 | |
| ]; | |
| console.log(` 参数: ${chromeArgs.join(' ')}`); // 打印所有参数 | |
| try { | |
| const chromeProcess = spawn( | |
| chromeExecutablePath, | |
| chromeArgs, // 使用包含窗口大小的参数数组 | |
| { detached: true, stdio: 'ignore' } // Detach to allow script to exit independently if needed | |
| ); | |
| chromeProcess.unref(); // Allow parent process to exit independently | |
| console.log(`${GREEN}✅ Chrome 启动命令已发送 (指定窗口大小)。稍后将尝试连接...${RESET}`); | |
| console.log(`${DIM}⏳ 等待 3 秒让 Chrome 进程启动...${RESET}`); | |
| await new Promise(resolve => setTimeout(resolve, 3000)); | |
| return true; // 表示启动流程已尝试 | |
| } catch (error) { | |
| console.error(`${RED}❌ 启动 Chrome 时出错: ${error.message}${RESET}`); | |
| console.error(` 请检查路径 "${chromeExecutablePath}" 是否正确,以及是否有权限执行。`); | |
| return false; | |
| } | |
| } | |
| // --- 步骤 3: 连接 Playwright 并管理页面 (带重试) --- | |
| async function connectAndManagePage() { | |
| console.log(`${CYAN}-------------------------------------------------${RESET}`); | |
| console.log(`${CYAN}--- 步骤 3: 连接 Playwright 到 ${CDP_ADDRESS} (最多尝试 ${CONNECTION_RETRIES} 次) ---${RESET}`); | |
| let browser = null; | |
| let context = null; | |
| for (let i = 0; i < CONNECTION_RETRIES; i++) { | |
| try { | |
| console.log(`\n${DIM}尝试连接 Playwright (第 ${i + 1}/${CONNECTION_RETRIES} 次)...${RESET}`); | |
| browser = await playwright.chromium.connectOverCDP(CDP_ADDRESS, { timeout: CONNECT_TIMEOUT_MS }); | |
| console.log(`${GREEN}✅ 成功连接到 Chrome!${RESET}`); | |
| // Simplified context fetching | |
| await new Promise(resolve => setTimeout(resolve, 500)); // Short delay after connect | |
| const contexts = browser.contexts(); | |
| if (contexts && contexts.length > 0) { | |
| context = contexts[0]; | |
| console.log(`-> 获取到浏览器默认上下文。`); | |
| break; // Connection and context successful | |
| } else { | |
| // This case should be rare if connectOverCDP succeeded with a responsive Chrome | |
| throw new Error('连接成功,但无法获取浏览器上下文。Chrome 可能没有响应或未完全初始化。'); | |
| } | |
| } catch (error) { | |
| console.warn(` ${YELLOW}连接尝试 ${i + 1} 失败: ${error.message.split('\n')[0]}${RESET}`); | |
| if (browser && browser.isConnected()) { | |
| // Should not happen if connectOverCDP failed, but good practice | |
| await browser.close().catch(e => console.error("尝试关闭连接失败的浏览器时出错:", e)); | |
| } | |
| browser = null; | |
| context = null; | |
| if (i < CONNECTION_RETRIES - 1) { | |
| console.log(` ${YELLOW}可能原因: Chrome 未完全启动 / 端口 ${DEBUGGING_PORT} 未监听 / 端口被占用。${RESET}`); | |
| console.log(`${DIM} 等待 ${RETRY_DELAY_MS / 1000} 秒后重试...${RESET}`); | |
| await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); | |
| } else { | |
| console.error(`\n${RED}❌ 在 ${CONNECTION_RETRIES} 次尝试后仍然无法连接。${RESET}`); | |
| console.error(' 请再次检查:'); | |
| console.error(' 1. Chrome 是否真的已经通过脚本成功启动,并且窗口可见、已加载?(可能需要登录Google)'); | |
| console.error(` 2. 是否有其他程序占用了端口 ${DEBUGGING_PORT}?(检查命令: macOS/Linux: lsof -i :${DEBUGGING_PORT} | Windows: netstat -ano | findstr ${DEBUGGING_PORT})`); | |
| console.error(' 3. 启动 Chrome 时终端或系统是否有报错信息?'); | |
| console.error(' 4. 防火墙或安全软件是否阻止了本地回环地址(127.0.0.1)的连接?'); | |
| return false; | |
| } | |
| } | |
| } | |
| if (!browser || !context) { | |
| console.error(`${RED}-> 未能成功连接到浏览器或获取上下文。${RESET}`); | |
| return false; | |
| } | |
| // --- 连接成功后的页面管理逻辑 --- | |
| console.log(`\n${CYAN}--- 页面管理 ---${RESET}`); | |
| try { | |
| let targetPage = null; | |
| let pages = []; | |
| try { | |
| pages = context.pages(); | |
| } catch (err) { | |
| console.error(`${RED}❌ 获取现有页面列表时出错:${RESET}`, err); | |
| console.log(" 将尝试打开新页面..."); | |
| } | |
| console.log(`${DIM}-> 检查 ${pages.length} 个已存在的页面...${RESET}`); | |
| const aiStudioUrlPattern = 'aistudio.google.com/'; | |
| const loginUrlPattern = 'accounts.google.com/'; | |
| for (const page of pages) { | |
| try { | |
| if (!page.isClosed()) { | |
| const pageUrl = page.url(); | |
| console.log(`${DIM} 检查页面: ${pageUrl}${RESET}`); | |
| // Prioritize AI Studio pages, then login pages | |
| if (pageUrl.includes(aiStudioUrlPattern)) { | |
| console.log(`-> ${GREEN}找到 AI Studio 页面:${RESET} ${pageUrl}`); | |
| targetPage = page; | |
| // Ensure it's the target URL if possible | |
| if (!pageUrl.includes('/prompts/new_chat')) { | |
| console.log(`${YELLOW} 非目标页面,尝试导航到 ${TARGET_URL}...${RESET}`); | |
| try { | |
| await targetPage.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT_MS }); | |
| console.log(` ${GREEN}导航成功:${RESET} ${targetPage.url()}`); | |
| } catch (navError) { | |
| console.warn(` ${YELLOW}警告:导航到 ${TARGET_URL} 失败: ${navError.message.split('\n')[0]}${RESET}`); | |
| console.warn(` ${YELLOW}将使用当前页面 (${pageUrl}),请稍后手动确认。${RESET}`); | |
| } | |
| } else { | |
| console.log(` ${GREEN}页面已在目标路径或子路径。${RESET}`); | |
| } | |
| break; // Found a good AI Studio page | |
| } else if (pageUrl.includes(loginUrlPattern) && !targetPage) { | |
| // Keep track of a login page if no AI studio page is found yet | |
| console.log(`-> ${YELLOW}发现 Google 登录页面,暂存。${RESET}`); | |
| targetPage = page; | |
| // Don't break here, keep looking for a direct AI Studio page | |
| } | |
| } | |
| } catch (pageError) { | |
| if (!page.isClosed()) { | |
| console.warn(` ${YELLOW}警告:评估或导航页面时出错: ${pageError.message.split('\n')[0]}${RESET}`); | |
| } | |
| // Avoid using a page that caused an error | |
| if (targetPage === page) { | |
| targetPage = null; | |
| } | |
| } | |
| } | |
| // If after checking all pages, the best we found was a login page | |
| if (targetPage && targetPage.url().includes(loginUrlPattern)) { | |
| console.log(`-> ${YELLOW}未找到直接的 AI Studio 页面,将使用之前找到的登录页面。${RESET}`); | |
| console.log(` ${YELLOW}请确保在该页面手动完成登录。${RESET}`); | |
| } | |
| // If no suitable page was found at all | |
| if (!targetPage) { | |
| console.log(`-> ${YELLOW}未找到合适的现有页面。正在打开新页面并导航到 ${TARGET_URL}...${RESET}`); | |
| try { | |
| targetPage = await context.newPage(); | |
| console.log(`${DIM} 正在导航...${RESET}`); | |
| await targetPage.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT_MS }); | |
| console.log(`-> ${GREEN}新页面已打开并导航到:${RESET} ${targetPage.url()}`); | |
| } catch (newPageError) { | |
| console.error(`${RED}❌ 打开或导航新页面到 ${TARGET_URL} 失败: ${newPageError.message}${RESET}`); | |
| console.error(" 请检查网络连接,以及 Chrome 是否能正常访问该网址。可能需要手动登录。" ); | |
| await browser.close().catch(e => {}); | |
| return false; | |
| } | |
| } | |
| try { | |
| await targetPage.bringToFront(); | |
| console.log('-> 已尝试将目标页面置于前台。'); | |
| } catch (bringToFrontError) { | |
| console.warn(` ${YELLOW}警告:将页面置于前台失败: ${bringToFrontError.message.split('\n')[0]}${RESET}`); | |
| console.warn(` (这可能发生在窗口最小化或位于不同虚拟桌面上时,通常不影响连接)`); | |
| } | |
| await new Promise(resolve => setTimeout(resolve, 500)); // Small delay after bringToFront | |
| console.log(`\n${BRIGHT}${GREEN}🎉 --- AI Studio 连接准备完成 --- 🎉${RESET}`); | |
| console.log(`${GREEN}Chrome 已启动,Playwright 已连接,相关页面已找到或创建。${RESET}`); | |
| console.log(`${YELLOW}请确保在 Chrome 窗口中 AI Studio 页面处于可交互状态 (例如,已登录Google, 无弹窗)。${RESET}`); | |
| return true; | |
| } catch (error) { | |
| console.error(`\n${RED}❌ --- 步骤 3 页面管理失败 ---${RESET}`); | |
| console.error(' 在连接成功后,处理页面时发生错误:', error); | |
| if (browser && browser.isConnected()) { | |
| await browser.close().catch(e => console.error("关闭浏览器时出错:", e)); | |
| } | |
| return false; | |
| } finally { | |
| // 这里不再打印即将退出的日志,因为脚本会继续运行 server.js | |
| // console.log("-> auto_connect_aistudio.js 步骤3结束。"); | |
| // 不需要手动断开 browser 连接,因为是 connectOverCDP | |
| } | |
| } | |
| // --- 步骤 4: 启动 API 服务器 --- | |
| function startApiServer() { | |
| console.log(`${CYAN}-------------------------------------------------${RESET}`); | |
| console.log(`${CYAN}--- 步骤 4: 启动 API 服务器 ('node ${SERVER_SCRIPT_FILENAME}') ---${RESET}`); | |
| console.log(`${DIM} 脚本路径: ${SERVER_SCRIPT_PATH}${RESET}`); | |
| if (!fs.existsSync(SERVER_SCRIPT_PATH)) { | |
| console.error(`${RED}❌ 错误: 无法启动服务器,文件不存在: ${SERVER_SCRIPT_PATH}${RESET}`); | |
| process.exit(1); | |
| } | |
| console.log(`${DIM}正在启动: node ${SERVER_SCRIPT_PATH}${RESET}`); | |
| try { | |
| const serverProcess = spawn('node', [SERVER_SCRIPT_PATH], { | |
| stdio: 'inherit', | |
| cwd: __dirname | |
| }); | |
| serverProcess.on('error', (err) => { | |
| console.error(`${RED}❌ 启动 '${SERVER_SCRIPT_FILENAME}' 失败: ${err.message}${RESET}`); | |
| console.error(`请检查 Node.js 是否已安装并配置在系统 PATH 中,以及 '${SERVER_SCRIPT_FILENAME}' 文件是否有效。`); | |
| process.exit(1); | |
| }); | |
| serverProcess.on('exit', (code, signal) => { | |
| console.log(`\n${MAGENTA}👋 '${SERVER_SCRIPT_FILENAME}' 进程已退出 (代码: ${code}, 信号: ${signal})。${RESET}`); | |
| console.log("自动连接脚本执行结束。"); | |
| process.exit(code ?? 0); | |
| }); | |
| // Don't print the success message here, let server.cjs print its own ready message | |
| // console.log("✅ '${SERVER_SCRIPT_FILENAME}' 已启动。脚本将保持运行,直到服务器进程结束或被手动中断。"); | |
| } catch (error) { | |
| console.error(`${RED}❌ 启动 '${SERVER_SCRIPT_FILENAME}' 时发生意外错误: ${error.message}${RESET}`); | |
| process.exit(1); | |
| } | |
| } | |
| // --- 主执行流程 --- | |
| (async () => { | |
| console.log(`${MAGENTA}🚀 欢迎使用 AI Studio 自动连接与启动脚本 (跨平台优化, v2.9 自动端口清理) 🚀${RESET}`); | |
| console.log(`${MAGENTA}=================================================${RESET}`); | |
| if (!await checkDependencies()) { | |
| process.exit(1); | |
| } | |
| console.log(`${MAGENTA}=================================================${RESET}`); | |
| const launchResult = await launchChrome(); | |
| if (launchResult === false) { | |
| console.log(`${RED}❌ 启动 Chrome 失败,脚本终止。${RESET}`); | |
| process.exit(1); | |
| } | |
| // 如果 launchResult 是 'use_existing' 或 true, 都需要连接 | |
| console.log(`${MAGENTA}=================================================${RESET}`); | |
| if (!await connectAndManagePage()) { | |
| // 如果连接失败,并且我们是尝试连接到现有实例,给出更具体的提示 | |
| if (launchResult === 'use_existing') { | |
| console.error(`${RED}❌ 连接到现有 Chrome 实例 (端口 ${DEBUGGING_PORT}) 失败。${RESET}`); | |
| console.error(' 请确认:'); | |
| console.error(' 1. 占用该端口的确实是您想连接的 Chrome 实例。'); | |
| console.error(' 2. 该 Chrome 实例是以 --remote-debugging-port 参数启动的。'); | |
| console.error(' 3. Chrome 实例本身运行正常,没有崩溃或无响应。'); | |
| } | |
| process.exit(1); | |
| } | |
| // 无论 Chrome 是新启动的还是已存在的,只要连接成功,就启动 API 服务器 | |
| console.log(`${MAGENTA}=================================================${RESET}`); | |
| startApiServer(); | |
| })(); |