Files
playwright-gitcode/main.js
2026-07-06 09:34:39 +08:00

370 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* GitCode 每日任务自动化脚本
* =========================
*
* 启动入口,按顺序执行:
* 1. 启动浏览器
* 2. 创建上下文(加载 state.json
* 3. 验证/执行登录ensureLogin
* 4. 扫描每日任务
* 5. 逐一执行未完成任务runTasks
* 6. 一键领取奖励
* 7. 发送飞书通知
* 8. 关闭浏览器
*/
const config = require('./config');
const logger = require('./utils/logger');
const { launchBrowser, createContext } = require('./utils/browser');
const { ensureLogin } = require('./utils/login');
const { runTasks } = require('./utils/taskRunner');
const { safeGoto, waitForElements } = require('./utils/page');
const { notifySuccess, sendScreenshotNotify } = require('./utils/feishu');
// 任务模块
const updateProject = require('./tasks/updateProject');
const dailyShare = require('./tasks/dailyShare');
const dailyCheckIn = require('./tasks/dailyCheckIn');
const dailyTrending = require('./tasks/dailyTrending');
const dailyStar = require('./tasks/dailyStar');
/**
* 判断任务是否已完成
* @param {import('playwright').Locator} task - 任务卡片元素
* @returns {Promise<boolean>} true=已完成
*/
async function taskIsCompleted(task) {
// 检测"领取奖励"按钮
const rewardBtn = task.locator('button:has-text("领取奖励")');
logger.info(' → 检查是否有可领取的奖励...');
const rewardCount = await rewardBtn.count();
logger.info(` → "领取奖励"按钮数量: ${rewardCount}`);
if (rewardCount > 0) {
logger.info('🎁 检测到可领取奖励,正在领取...');
logger.info(' → 点击领取奖励按钮...');
await rewardBtn.click();
logger.info(' → 等待 800ms 刷新状态...');
await task.page().waitForTimeout(800);
const stillHasRewardBtn = await rewardBtn.count() > 0;
const isCompleted = await task.locator('.completed').count() > 0;
logger.info(` → 奖励按钮仍存在: ${stillHasRewardBtn}, 已完成标记: ${isCompleted}`);
if (!stillHasRewardBtn || isCompleted) {
logger.info('✅ 奖励领取成功');
return true;
}
logger.warn('⚠️ 已点击领取奖励,但状态未立即变化,按已完成处理避免重复执行任务');
return true;
}
// 检测已完成标记
const isCompleted = await task.locator('.completed').count() > 0;
logger.info(` → 已完成标记(.completed): ${isCompleted}`);
if (isCompleted) {
logger.info('✅ 已完成,跳过');
return true;
}
logger.info(' → 任务未完成,需要执行');
return false;
}
/**
* 查找任务卡片(多选择器兜底 + 自动重试)
* @param {import('playwright').Page} page
* @returns {Promise<import('playwright').Locator[]>}
*/
async function findTaskCards(page) {
return waitForElements(page, [
'.novice-task-item-box',
'[class*="task-item"]',
'[class*="Task"]',
'[class*="novice"]',
'.item-box',
'[class*="card"]',
], {
interval: 1000,
maxRetries: 10,
minCount: 1,
});
}
/**
* 主函数
*/
async function main() {
let browser = null;
let page = null;
try {
// ── 1. 启动浏览器 ──
logger.divider();
logger.info('===== 第1步: 启动浏览器 =====');
browser = await launchBrowser();
// ── 2. 创建上下文 ──
logger.divider();
logger.info('===== 第2步: 创建浏览器上下文 =====');
const { context } = await createContext(browser);
logger.info('创建新页面...');
page = await context.newPage();
logger.info('✅ 新页面已创建');
// ── 3. 登录验证 ──
logger.divider();
logger.info('===== 第3步: 登录验证 =====');
await ensureLogin(page, context);
// ── 4. 扫描每日任务 ──
logger.divider();
logger.info('===== 第4步: 扫描每日任务 =====');
logger.info('重新访问积分中心页面(确保最新状态)...');
await safeGoto(page, config.urls.points, {
label: '积分中心',
timeout: config.navigationTimeout,
waitFor: [
'.g-user-avatar',
'text=密码登录',
],
});
// ── 查找任务卡片(支持多选择器兜底) ──
logger.info('正在查找任务卡片元素...');
let taskElements = await findTaskCards(page);
logger.info(`共找到 ${taskElements.length} 个任务卡片`);
if (taskElements.length === 0) {
// 终极诊断dump 页面关键区域的 HTML
logger.warn('❌ 所有选择器均未找到任务卡片,开始诊断...');
try {
const title = await page.title();
const url = page.url();
logger.info(`当前URL: ${url}`);
logger.info(`页面标题: ${title}`);
// 截取页面 body 中前 5000 字符的 HTML 用于诊断
const bodyHTML = await page.evaluate(() => {
// 找最有可能包含任务区域的父容器
const main = document.querySelector('main') ||
document.querySelector('.main-content') ||
document.querySelector('.page-content') ||
document.querySelector('#app') ||
document.body;
return (main instanceof HTMLElement ? main.innerHTML : document.body.innerHTML).substring(0, 5000);
});
logger.info('===== 页面关键区域 HTML (前5000字符) =====');
// 分行输出以便阅读
const lines = bodyHTML.split('\n').filter(l => l.trim());
for (let i = 0; i < Math.min(lines.length, 100); i++) {
const line = lines[i].trim();
if (line) logger.info(` ${line}`);
}
logger.info('===== HTML 输出结束 =====');
// 额外:列出页面所有 class 名称去重(帮助找正确的选择器)
const allClasses = await page.evaluate(() => {
const all = document.querySelectorAll('[class]');
const set = new Set();
all.forEach(el => {
el.className.split(/\\s+/).forEach(c => {
if (c && (c.includes('task') || c.includes('Task') || c.includes('novice') || c.includes('item') || c.includes('card'))) {
set.add(c);
}
});
});
return Array.from(set).sort();
});
if (allClasses.length > 0) {
logger.info('包含 task/novice/item/card 的 class 列表:');
allClasses.forEach(c => logger.info(` .${c}`));
}
} catch (diagErr) {
logger.warn(`诊断过程出错: ${diagErr.message}`);
}
throw new Error(
'未找到任务卡片,页面结构可能已变化。\n' +
'请查看上方 "页面关键区域 HTML" 和 class 列表,\n' +
'将包含任务卡片的 HTML 片段和正确类名发给我,我来更新选择器。'
);
}
// ── 5. 构建待执行任务列表 ──
logger.divider();
logger.info('===== 第5步: 构建待执行任务列表 =====');
const pendingTasks = [];
for (let i = 0; i < taskElements.length; i++) {
const taskElement = taskElements[i];
logger.info(`--- 任务卡片 #${i + 1} ---`);
let title = '';
try {
title = await taskElement.locator('.task-card-title__label-text').innerText();
logger.info(`任务标题: "${title}"`);
} catch (err) {
logger.warn(` → 获取标题失败: ${err.message},跳过此卡片`);
continue;
}
logger.info('检查完成状态...');
const completed = await taskIsCompleted(taskElement);
if (completed) {
logger.info(` → "${title}" 已完成,跳过`);
continue;
}
// 根据标题匹配任务
if (title.includes('更新项目')) {
logger.info(` → 匹配到任务: 更新项目`);
pendingTasks.push({ name: '更新项目', handler: updateProject });
} else if (title.includes('每日分享')) {
logger.info(` → 匹配到任务: 每日分享`);
pendingTasks.push({ name: '每日分享', handler: dailyShare });
} else if (title.includes('每日签到')) {
logger.info(` → 匹配到任务: 每日签到`);
pendingTasks.push({ name: '每日签到', handler: dailyCheckIn });
} else if (title.includes('查看热门')) {
logger.info(` → 匹配到任务: 查看热门`);
pendingTasks.push({ name: '查看热门', handler: dailyTrending });
} else if (title.includes('每日Star')) {
logger.info(` → 匹配到任务: 每日Star`);
pendingTasks.push({ name: '每日Star', handler: dailyStar });
} else {
logger.warn(` → 未知任务类型: "${title}",跳过`);
}
}
logger.info(`待执行任务: ${pendingTasks.length}`);
if (pendingTasks.length === 0) {
logger.info('所有任务已完成,无需执行');
} else {
// ── 6. 执行任务(互不影响) ──
logger.divider();
logger.info('===== 第6步: 依次执行任务 =====');
const results = await runTasks(page, pendingTasks);
// ── 7. 一键领取奖励 ──
logger.divider();
logger.info('===== 第7步: 一键领取奖励 =====');
logger.info('重新访问积分中心页面...');
await safeGoto(page, config.urls.points, {
label: '积分中心',
timeout: config.navigationTimeout,
waitFor: ['.g-user-avatar'],
});
logger.info('查找"一键领取"按钮...');
const claimBtns = await waitForElements(page, [
'button:has-text("一键领取")',
'button.pickup-btn',
'[class*="pickup"]',
'button:has-text("领取")',
], {
interval: 1000,
maxRetries: 6,
minCount: 1,
});
const claimVisible = claimBtns.length > 0;
const claimBtn = claimBtns.length > 0 ? claimBtns[0] : null;
logger.info(` → 按钮可见: ${claimVisible}`);
if (claimBtn) {
logger.info(' → 点击一键领取...');
try {
await claimBtn.click({ timeout: 5000 });
logger.info(' → 等待 2s...');
await page.waitForTimeout(2000);
logger.info('✅ 一键领取已点击');
await safeGoto(page, config.urls.points, {
label: '积分中心',
timeout: config.navigationTimeout,
waitFor: ['.g-user-avatar'],
});
} catch (err) {
logger.error(`点击一键领取失败: ${err.message}`);
}
} else {
logger.info('没有可领取的一键奖励');
}
// ── 8. 打印所有任务完成状态 ──
logger.divider();
logger.info('===== 第8步: 最终任务状态检查 =====');
const finalTasks = await findTaskCards(page);
logger.info(`${finalTasks.length} 个任务卡片`);
for (const task of finalTasks) {
try {
const title = await task.locator('.task-card-title__label-text').innerText();
const isCompleted = await task.locator('.completed').count() > 0;
logger.info(` ${isCompleted ? '✅' : '❌'} ${title} ${isCompleted ? '已完成' : '未完成'}`);
} catch (_) {
// 跳过
}
}
// ── 9. 飞书通知 ──
logger.divider();
logger.info('===== 第9步: 发送飞书通知 =====');
// 最终结果截图+飞书
sendScreenshotNotify(page, '每日任务', true).catch(() => { });
await notifySuccess(results);
}
logger.divider();
logger.info('✅ 全部流程执行完毕');
} catch (err) {
logger.divider();
logger.error(`❌ 主流程异常: ${err.message}`);
logger.error(err.stack);
// 截图+飞书通知
if (typeof page !== 'undefined') {
sendScreenshotNotify(page, '主流程', false, err.message).catch(() => { });
} else {
const feishu = require('./utils/feishu');
feishu.sendFeishuText(`❌ 脚本执行异常\n\n${err.message}`).catch(() => { });
}
} finally {
// ── 10. 关闭浏览器 ──
logger.divider();
logger.info('===== 第10步: 关闭浏览器 =====');
if (browser) {
try {
logger.info('正在关闭浏览器...');
await browser.close();
logger.info('✅ 浏览器已关闭');
} catch (closeErr) {
logger.warn(`关闭浏览器时出现异常: ${closeErr.message}`);
}
}
process.exit(0)
}
}
// 执行主函数
async function runWithTimeout() {
const timeoutMs = 25 * 60 * 1000; // 25分钟
return Promise.race([
main(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`任务超时 ${timeoutMs}ms`)), timeoutMs)
),
]);
}
runWithTimeout()
.then(() => {
logger.info('任务完成');
process.exit(0);
})
.catch(err => {
logger.error(`任务失败: ${err.stack || err.message}`);
process.exit(1);
});