224 lines
6.3 KiB
JavaScript
224 lines
6.3 KiB
JavaScript
/**
|
||
* 任务:每日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;
|