Refactor GitCode daily bot

This commit is contained in:
2026-07-06 09:34:39 +08:00
parent 120ae7d3f9
commit 77a07aae0a
19 changed files with 2251 additions and 533 deletions

369
main.js Normal file
View File

@@ -0,0 +1,369 @@
/**
* 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);
});