186 lines
6.8 KiB
JavaScript
186 lines
6.8 KiB
JavaScript
/**
|
||
* 登录模块
|
||
* 核心函数 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<boolean>} 登录是否成功
|
||
* @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<string>} 截图文件路径
|
||
*/
|
||
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,
|
||
};
|