Refactor GitCode daily bot
This commit is contained in:
50
tasks/dailyCheckIn.js
Normal file
50
tasks/dailyCheckIn.js
Normal 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
68
tasks/dailyShare.js
Normal 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
228
tasks/dailyStar.js
Normal 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
111
tasks/dailyTrending.js
Normal 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
66
tasks/updateProject.js
Normal 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;
|
||||
Reference in New Issue
Block a user