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

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;