/** * 登录模块 * 核心函数 ensureLogin(page, context) * * 流程: * 1. 访问 积分中心页面 * 2. 检测 .g-user-avatar 是否存在 * 3. 如果已登录 → 返回 true * 4. 如果未登录 → 执行账号密码登录 → 保存 state.json * 5. 登录失败 → 抛出异常,阻止后续任务 * * 【日志规范】每个 await 前后都有日志,定位卡顿时精确到行 */ const config = require('../config'); const logger = require('./logger'); const { saveState } = require('./browser'); const { safeGoto } = require('./page'); /** * 确保已登录,若未登录则执行登录流程 * @param {import('playwright').Page} page * @param {import('playwright').BrowserContext} context * @returns {Promise} 登录是否成功 * @throws {Error} 登录失败时抛出异常 */ async function ensureLogin(page, context) { logger.divider(); logger.info('===== 开始检查登录状态 ====='); // ── 步骤1: 访问积分中心页面 ── logger.info('[step 1/8] 正在访问积分中心页面...'); const pageReady = await safeGoto(page, config.urls.points, { label: '积分中心', timeout: config.navigationTimeout, waitFor: [ '.g-user-avatar', 'text=密码登录', ], }); if (!pageReady) { logger.warn('[step 1/8] ⚠️ 关键元素未全部出现,尝试继续...'); } else { logger.info('[step 1/8] ✅ 积分中心页面加载完成'); } // ── 步骤2: 打印当前页面信息 ── logger.info('[step 2/8] 获取当前页面信息...'); try { const currentUrl = page.url(); const pageTitle = await page.title(); logger.info(` → 当前URL: ${currentUrl}`); logger.info(` → 页面标题: ${pageTitle}`); } catch (err) { logger.warn(` → 获取页面信息失败: ${err.message}`); } // ── 步骤3: 检测登录状态 ── logger.info('[step 3/8] 检测登录状态(查找 .g-user-avatar)...'); let isLoggedIn = false; try { const avatarLocator = page.locator('.g-user-avatar'); logger.info(' → 调用 isVisible()...'); isLoggedIn = await avatarLocator.isVisible(); logger.info(` → isVisible() 返回: ${isLoggedIn}`); } catch (err) { logger.warn(` → 检测登录状态时出错: ${err.message}`); isLoggedIn = false; } if (isLoggedIn) { logger.info('✅ 已登录,无需执行登录流程'); return true; } // ── 步骤4: 未登录,开始登录 ── logger.info('❌ 未登录,开始执行登录流程'); logger.info('[step 4/8] 等待页面稳定 (waitForTimeout 1000ms)...'); await page.waitForTimeout(1000); try { // ── 步骤4a: 点击密码登录 ── logger.info('[step 4a] 点击"密码登录"标签...'); const pwdLoginBtn = page.getByText('密码登录'); logger.info(' → 等待密码登录按钮可见...'); await pwdLoginBtn.waitFor({ state: 'visible', timeout: 10000 }); logger.info(' → 点击密码登录按钮...'); await pwdLoginBtn.click(); logger.info('✅ 已切换到密码登录方式'); // ── 步骤4b: 填写用户名 ── logger.info('[step 4b] 填写用户名...'); const usernameInput = page.getByPlaceholder('请填写手机号/用户名/邮箱'); logger.info(' → 等待输入框可见...'); await usernameInput.waitFor({ state: 'visible', timeout: 10000 }); logger.info(` → 输入用户名: ${config.login.username}`); await usernameInput.fill(config.login.username); logger.info('✅ 用户名已填写'); // ── 步骤4c: 填写密码 ── logger.info('[step 4c] 填写密码...'); const passwordInput = page.getByPlaceholder('请填写密码'); logger.info(' → 等待密码输入框可见...'); await passwordInput.waitFor({ state: 'visible', timeout: 10000 }); logger.info(' → 输入密码 (已隐藏)'); await passwordInput.fill(config.login.password); logger.info('✅ 密码已填写'); // ── 步骤4d: 勾选协议 ── logger.info('[step 4d] 勾选同意协议...'); const agree1 = page.getByText('登录或注册完成代表你同意'); logger.info(' → 点击第一个勾选项...'); await agree1.click(); const agree2 = page.getByText('我同意将账号、组织、仓库等信息提供给AtomGit进行 '); logger.info(' → 点击第二个勾选项...'); await agree2.click(); logger.info('✅ 协议已勾选'); // ── 步骤4e: 点击登录按钮 ── logger.info('[step 4e] 点击登录按钮...'); const loginBtn = page.locator('button:has-text("登 录")'); logger.info(' → 等待登录按钮可见...'); await loginBtn.waitFor({ state: 'visible', timeout: 10000 }); logger.info(' → 点击登录按钮...'); await loginBtn.click(); logger.info('✅ 登录按钮已点击,等待跳转...'); // ── 步骤4f: 等待跳转 ── logger.info('[step 4f] 等待页面跳转到积分中心...'); logger.info(` → waitForURL('**/setting/points'), timeout: ${config.navigationTimeout}ms`); await page.waitForURL('**/setting/points', { timeout: config.navigationTimeout, }); logger.info('✅ 页面已跳转到积分中心'); // ── 步骤4g: 验证登录成功 ── logger.info('[step 4g] 验证登录成功(查找 .g-user-avatar)...'); logger.info(` → waitForSelector('.g-user-avatar'), timeout: ${config.navigationTimeout}ms`); await page.waitForSelector('.g-user-avatar', { timeout: config.navigationTimeout, }); logger.info('✅ 登录成功,用户头像已出现'); // ── 步骤5: 保存状态 ── logger.info('[step 5/8] 保存登录状态到 state.json...'); await saveState(context); logger.info('✅ state.json 已保存'); return true; } catch (err) { // 登录失败:截图并抛异常 logger.error(`❌ 登录流程异常: ${err.message}`); const screenshotPath = await takeScreenshot(page, 'login-failed'); logger.info(`截图已保存: ${screenshotPath}`); throw new Error(`登录失败,无法继续执行任务: ${err.message}`); } } /** * 页面截图辅助 * @param {import('playwright').Page} page * @param {string} label 截图标签 * @returns {Promise} 截图文件路径 */ async function takeScreenshot(page, label) { const fs = require('fs'); const path = require('path'); const config = require('../config'); const dir = path.resolve(config.dirs.screenshots); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const filename = `error-${label}-${Date.now()}.png`; const filepath = path.join(dir, filename); logger.info(' → 正在截图...'); await page.screenshot({ path: filepath, fullPage: true }).catch(() => { }); return filepath; } module.exports = { ensureLogin, };