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

50
tasks/dailyCheckIn.js Normal file
View File

@@ -0,0 +1,50 @@
/**
* 任务:每日签到
*
* 逻辑:
* 1. 找到"每日签到"任务卡片
* 2. 点击"签到"按钮
*/
const path = require('path');
const logger = require(path.join(__dirname, '..', 'utils', 'logger'));
/**
* 执行每日签到
* @param {import('playwright').Page} page - 主页面
*/
async function dailyCheckIn(page) {
logger.info('👉 执行每日签到');
// 从主页面找到"每日签到"任务卡片
const tasks = await page.locator('.novice-task-item-box').all();
let found = false;
for (const task of tasks) {
try {
const text = await task.locator('.task-card-title__label-text').innerText();
if (text.includes('每日签到')) {
const signInBtn = task.locator('button:has-text("签到")');
const isVisible = await signInBtn.isVisible().catch(() => false);
if (isVisible) {
await signInBtn.click();
logger.info('签到按钮已点击');
found = true;
} else {
logger.info('签到按钮不可见,可能已签到');
found = true;
}
break;
}
} catch (_) {
// 跳过定位失败的元素
}
}
if (!found) {
throw new Error('未找到"每日签到"任务卡片或签到按钮');
}
}
module.exports = dailyCheckIn;

68
tasks/dailyShare.js Normal file
View File

@@ -0,0 +1,68 @@
/**
* 任务:每日分享
*
* 逻辑:
* 1. 点击任务卡片上的 .toComplete 按钮
* 2. 等待新页面打开
* 3. 点击邀请按钮
* 4. 关闭新页面
*/
const path = require('path');
const { openNewPage, safeClose } = require(path.join(__dirname, '..', 'utils', 'page'));
const logger = require(path.join(__dirname, '..', 'utils', 'logger'));
/**
* 查找并点击任务卡片中的指定按钮
* @param {import('playwright').Page} page - 主页面
*/
async function dailyShare(page) {
logger.info('👉 执行每日分享');
// 从主页面找到"每日分享"任务卡片
const tasks = await page.locator('.novice-task-item-box').all();
const shareTask = await findTaskByTitle(tasks, '每日分享');
if (!shareTask) {
throw new Error('未找到"每日分享"任务卡片');
}
// 点击 toComplete 打开分享页
const newPage = await openNewPage(
page.context(),
() => shareTask.locator('.toComplete').click()
);
try {
// 点击第一个"立即邀请"按钮
await newPage.locator('.immediately-invite-btn', {
hasText: '立即邀请'
}).click();
logger.info('分享操作已完成');
await newPage.waitForTimeout(3000);
} finally {
await safeClose(newPage);
}
}
/**
* 根据标题查找任务卡片
* @param {import('playwright').Locator[]} tasks
* @param {string} title
* @returns {Promise<import('playwright').Locator|null>}
*/
async function findTaskByTitle(tasks, title) {
for (const task of tasks) {
try {
const text = await task.locator('.task-card-title__label-text').innerText();
if (text.includes(title)) {
return task;
}
} catch (_) {
// 单个元素定位失败则跳过
}
}
return null;
}
module.exports = dailyShare;

228
tasks/dailyStar.js Normal file
View File

@@ -0,0 +1,228 @@
/**
* 任务每日Star
*
* 逻辑:
* 1. 找到"每日Star"任务卡片
* 2. 点击 toComplete 打开热门列表
* 3. 随机选择一个热门项目
* 4. 进入项目详情页
* 5. 找到 Star/Starred 按钮
* 6. 如果已 Starred → 先取消再重新 Star
* 7. 如果未 Star → 直接 Star
*
* 定位器优化:
* - 优先文本匹配Star / Starred
* - 不依赖固定 nth 索引
* - 自动打印页面 title 和 URL
* - 找不到按钮抛异常
*/
const path = require('path');
const logger = require(path.join(__dirname, '..', 'utils', 'logger'));
const {
openNewPage,
openLinkPage,
safeClose,
getPageInfo,
waitForElements,
} = require(path.join(__dirname, '..', 'utils', 'page'));
const PROJECT_CARD_SELECTORS = [
'.hot-selection-list a.recommend-card',
'.hot-selection .recommend-card',
'a.recommend-card',
'a[href*="source_module=home_hot_selection"]',
'a[href*="source_module=home_recommend"]',
'a[href*="/"][href*="?source_module="]',
];
/**
* 执行每日Star任务
* @param {import('playwright').Page} page - 主页面
*/
async function dailyStar(page) {
logger.info('👉 执行每日Star');
// 从主页面找到"每日Star"任务卡片
const tasks = await page.locator('.novice-task-item-box').all();
const starTask = await findTaskByTitle(tasks, '每日Star');
if (!starTask) {
throw new Error('未找到"每日Star"任务卡片');
}
// ── 第一步:打开热门项目列表页 ──
const listPage = await openNewPage(
page.context(),
() => starTask.locator('.toComplete').click()
);
try {
const cards = await findProjectCards(listPage);
const count = cards.length;
logger.info(`热门项目数量: ${count}`);
if (count === 0) {
throw new Error('没有找到热门项目');
}
// 随机选择一个项目
const index = Math.floor(Math.random() * count);
logger.info(`随机选择项目索引: ${index}`);
// ── 第二步:打开项目详情页 ──
const projectPage = await openLinkPage(
listPage.context(),
cards[index],
{ label: 'Star 项目详情页' }
);
try {
// 打印页面信息
const info = await getPageInfo(projectPage);
logger.info(`项目页面: ${info.title}`);
logger.info(`项目URL: ${info.url}`);
// ── 第三步:找到 Star/Starred 按钮 ──
await doStarOperation(projectPage);
} finally {
await safeClose(projectPage);
}
} finally {
await safeClose(listPage);
}
}
async function findProjectCards(page) {
const cards = await waitForElements(page, PROJECT_CARD_SELECTORS, {
interval: 1000,
maxRetries: 15,
minCount: 1,
});
if (cards.length > 0) {
return cards;
}
const info = await getPageInfo(page);
throw new Error(
`未找到热门项目卡片,页面结构可能已变化。页面标题: ${info.title}, 页面URL: ${info.url}`
);
}
/**
* 执行 Star/Unstar 操作
*
* 定位策略:
* 1. 在 #repo-header-tab 区域内查找包含 "Star" 或 "Starred" 文本的按钮
* 2. 优先匹配 .status-btn 类
* 3. 使用文本过滤而非 nth 索引
*
* @param {import('playwright').Page} page
*/
async function doStarOperation(page) {
// 等待页面加载
await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {});
logger.info('项目详情页加载完成');
// 定位 Star 按钮区域
const headerTab = page.locator('#repo-header-tab');
await headerTab.waitFor({ state: 'attached', timeout: 15000 }).catch(() => {
// 如果 #repo-header-tab 不存在,尝试更通用的定位
logger.warn('#repo-header-tab 未找到,尝试更通用的定位方式');
});
// 查找 Star/Starred 按钮
// 策略1: #repo-header-tab 下的 .status-btn
let starBtn = page.locator('#repo-header-tab .status-btn').filter({
hasText: /Star/i,
});
let btnCount = await starBtn.count();
logger.info(`#repo-header-tab .status-btn 含 Star 文本的按钮数量: ${btnCount}`);
// 策略2: 如果策略1没找到全局搜索含 Star 的按钮
if (btnCount === 0) {
logger.warn('未在 #repo-header-tab 下找到 Star 按钮,全局搜索...');
starBtn = page.locator('button, a, .status-btn, .btn').filter({
hasText: /Star/i,
});
btnCount = await starBtn.count();
logger.info(`全局搜索含 Star 文本的按钮数量: ${btnCount}`);
}
// 策略3: 包含 ⭐ 或 svg star 图标
if (btnCount === 0) {
logger.warn('按文本未找到,尝试通过 aria-label 匹配...');
starBtn = page.locator('[aria-label*="star" i], [aria-label*="Star" i]');
btnCount = await starBtn.count();
logger.info(`通过 aria-label 找到的按钮数量: ${btnCount}`);
}
if (btnCount === 0) {
// 最终失败:截图并抛异常
const pageInfo = await getPageInfo(page);
throw new Error(
`Star按钮不存在\n` +
`页面标题: ${pageInfo.title}\n` +
`页面URL: ${pageInfo.url}\n` +
`共尝试了3种定位策略均未找到 Star/Starred 按钮`
);
}
// 取第一个匹配的按钮
const btn = starBtn.nth(btnCount > 1 ? 1 : 0);
const btnText = await btn.innerText().catch(() => '(获取文本失败)');
logger.info(`找到按钮,文本内容: "${btnText}"`);
// 判断当前状态
const isStarred = btnText.toLowerCase().includes('starred');
if (isStarred) {
// 已 Starred → 先取消
logger.info('当前状态: 已 Starred先取消');
await btn.click();
// 等待按钮文本变为 Star
await page.waitForTimeout(800);
const newText = await btn.innerText().catch(() => '');
logger.info(`取消后按钮文本: "${newText}"`);
}
// 点击 Star
logger.info('点击 Star 按钮');
await btn.click();
// 确认操作成功(等待按钮文本变化)
await page.waitForTimeout(1000);
const finalText = await btn.innerText().catch(() => '');
logger.info(`Star操作完成当前按钮文本: "${finalText}"`);
if (finalText.toLowerCase().includes('starred')) {
logger.info('✅ Star 操作成功确认');
} else {
logger.warn('Star 操作后按钮状态未确认,但操作已执行');
}
}
/**
* 根据标题查找任务卡片
*/
async function findTaskByTitle(tasks, title) {
for (const task of tasks) {
try {
const text = await task.locator('.task-card-title__label-text').innerText();
if (text.includes(title)) {
return task;
}
} catch (_) {
// 跳过
}
}
return null;
}
module.exports = dailyStar;

111
tasks/dailyTrending.js Normal file
View File

@@ -0,0 +1,111 @@
/**
* 任务:查看热门
*
* 逻辑:
* 1. 找到"查看热门"任务卡片
* 2. 点击 toComplete 打开热门页面
* 3. 点击第一个热门项目卡片
* 4. 在新页面停留后关闭
*/
const path = require('path');
const logger = require(path.join(__dirname, '..', 'utils', 'logger'));
const {
openNewPage,
openLinkPage,
safeClose,
waitPageReady,
waitForElements,
getPageInfo,
} = require(path.join(__dirname, '..', 'utils', 'page'));
const PROJECT_CARD_SELECTORS = [
'.hot-selection .recommend-card',
'.hot-selection-list a.recommend-card',
'a.recommend-card',
'a[href*="source_module=home_hot_selection"]',
'a[href*="source_module=home_recommend"]',
'a[href*="/"][href*="?source_module="]',
];
/**
* 执行查看热门任务
* @param {import('playwright').Page} page - 主页面
*/
async function dailyTrending(page) {
logger.info('👉 执行查看热门');
// 从主页面找到"查看热门"任务卡片
const tasks = await page.locator('.novice-task-item-box').all();
const trendingTask = await findTaskByTitle(tasks, '查看热门');
if (!trendingTask) {
throw new Error('未找到"查看热门"任务卡片');
}
// ── 第一步:打开热门页面 ──
const trendingPage = await openNewPage(
page.context(),
() => trendingTask.locator('.toComplete').click()
);
try {
const cards = await findProjectCards(trendingPage);
// ── 第二步:点击第一个热门项目,打开详情页 ──
const firstCard = cards[0];
const detailPage = await openLinkPage(
trendingPage.context(),
firstCard,
{ label: '热门项目详情页' }
);
try {
// 等待详情页加载
await waitPageReady(detailPage);
logger.info('热门项目详情页已打开');
} finally {
await safeClose(detailPage);
}
} finally {
await safeClose(trendingPage);
}
}
async function findProjectCards(page) {
const cards = await waitForElements(page, PROJECT_CARD_SELECTORS, {
interval: 1000,
maxRetries: 15,
minCount: 1,
});
if (cards.length > 0) {
logger.info(`热门项目候选数量: ${cards.length}`);
return cards;
}
const info = await getPageInfo(page);
throw new Error(
`未找到热门项目卡片,页面结构可能已变化。页面标题: ${info.title}, 页面URL: ${info.url}`
);
}
/**
* 根据标题查找任务卡片
*/
async function findTaskByTitle(tasks, title) {
for (const task of tasks) {
try {
const text = await task.locator('.task-card-title__label-text').innerText();
if (text.includes(title)) {
return task;
}
} catch (_) {
// 跳过
}
}
return null;
}
module.exports = dailyTrending;

66
tasks/updateProject.js Normal file
View File

@@ -0,0 +1,66 @@
/**
* 任务:更新项目
*
* 逻辑:
* 1. 打开仓库编辑页面
* 2. 在 Monaco 编辑器末尾追加内容
* 3. 点击提交修改按钮
*/
const path = require('path');
const config = require(path.join(__dirname, '..', 'config'));
const logger = require(path.join(__dirname, '..', 'utils', 'logger'));
const { safeClose, safeGoto } = require(path.join(__dirname, '..', 'utils', 'page'));
/**
* 执行更新项目任务
* @param {import('playwright').Page} page - 主页面
*/
async function updateProject(page) {
logger.info('👉 执行项目更新');
const context = page.context();
const newPage = await context.newPage();
try {
// 打开编辑页面
await safeGoto(newPage, config.urls.testRepo, {
label: '仓库编辑页',
timeout: config.navigationTimeout,
waitFor: ['#codeartside'],
});
logger.info('已进入仓库编辑页面');
// 获取编辑器 iframe
const frame = newPage.frameLocator('#codeartside');
// 点击编辑器区域
await frame.locator('.monaco-editor').click({
position: { x: 200, y: 100 },
});
// 光标移动到末尾
await newPage.keyboard.press('Control+End');
await newPage.waitForTimeout(300); // 极短等待确保光标到位(键盘事件无法 await
// 追加内容
const content = `\n# 自动更新 - ${new Date().toISOString()}`;
await newPage.keyboard.type(content, { delay: 50 });
logger.info('已写入内容');
// 点击"提交修改"按钮(页面上的第一个)
await newPage.locator('button:has-text("提交修改")').first().click();
await page.waitForTimeout(1000);
// 等待弹框中的"提交修改"按钮出现并点击
const confirmBtn = newPage.locator('button:has-text("提交修改")').nth(1);
await confirmBtn.waitFor({ state: 'visible', timeout: 10000 });
await confirmBtn.click();
await page.waitForTimeout(1000);
logger.info('项目更新已提交');
} finally {
await safeClose(newPage);
}
}
module.exports = updateProject;