/** * 任务:每日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 = [ '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;