/** * 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} 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} */ 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); });