diff --git a/debugTask.js b/debugTask.js new file mode 100644 index 0000000..ed64796 --- /dev/null +++ b/debugTask.js @@ -0,0 +1,127 @@ +/** + * 单任务调试入口 + * + * 示例: + * TASK=dailyTrending DEBUG_TASK=true DEBUG_KEEP_OPEN=true node debugTask.js + * TASK=updateProject DEBUG_TASK=true DEBUG_KEEP_OPEN=true DEBUG_DRY_RUN=true node debugTask.js + */ + +if (!process.env.DEBUG) { + process.env.DEBUG = 'true'; +} +if (!process.env.DEBUG_TASK) { + process.env.DEBUG_TASK = 'true'; +} + +const config = require('./config'); +const logger = require('./utils/logger'); +const { launchBrowser, createContext } = require('./utils/browser'); +const { ensureLogin } = require('./utils/login'); +const { safeGoto } = require('./utils/page'); + +const updateProject = require('./tasks/updateProject'); +const dailyShare = require('./tasks/dailyShare'); +const dailyCheckIn = require('./tasks/dailyCheckIn'); +const dailyTrending = require('./tasks/dailyTrending'); +const dailyStar = require('./tasks/dailyStar'); + +const taskMap = { + updateProject: { + name: '更新项目', + handler: updateProject, + }, + dailyTrending: { + name: '查看热门', + handler: dailyTrending, + }, + trending: { + name: '查看热门', + handler: dailyTrending, + }, + dailyShare: { + name: '每日分享', + handler: dailyShare, + }, + dailyCheckIn: { + name: '每日签到', + handler: dailyCheckIn, + }, + dailyStar: { + name: '每日Star', + handler: dailyStar, + }, +}; + +function printUsage() { + logger.info('用法: TASK= node debugTask.js'); + logger.info('可用任务:'); + Object.keys(taskMap).forEach((key) => logger.info(` ${key}`)); + logger.info('常用参数: DEBUG_KEEP_OPEN=true DEBUG_PAUSE_EACH_STEP=true DEBUG_DRY_RUN=true'); +} + +async function main() { + const taskKey = process.env.TASK; + const task = taskMap[taskKey]; + + if (!task) { + printUsage(); + throw new Error(`未知或未指定 TASK: ${taskKey || '(empty)'}`); + } + + let browser = null; + let context = null; + let page = null; + + try { + logger.divider(); + logger.info(`===== 单任务调试: ${task.name} (${taskKey}) =====`); + + browser = await launchBrowser(); + const created = await createContext(browser); + context = created.context; + page = await context.newPage(); + + logger.divider(); + logger.info('===== 登录验证 ====='); + await ensureLogin(page, context); + + logger.divider(); + logger.info('===== 打开积分中心 ====='); + await safeGoto(page, config.urls.points, { + label: '积分中心', + timeout: config.navigationTimeout, + waitFor: [ + '.g-user-avatar', + 'text=密码登录', + '.novice-task-item-box', + ], + }); + + logger.divider(); + logger.info(`===== 执行单任务: ${task.name} =====`); + await task.handler(page, { + debugTask: true, + keepOpen: process.env.DEBUG_KEEP_OPEN === 'true', + pauseEachStep: process.env.DEBUG_PAUSE_EACH_STEP === 'true', + dryRun: process.env.DEBUG_DRY_RUN === 'true', + }); + + logger.divider(); + logger.info(`✅ 单任务调试完成: ${task.name}`); + } catch (err) { + logger.divider(); + logger.error(`❌ 单任务调试失败: ${err.message}`); + logger.error(err.stack || err.message); + process.exitCode = 1; + } finally { + logger.divider(); + logger.info('===== 关闭浏览器 ====='); + if (browser) { + await browser.close().catch((err) => { + logger.warn(`关闭浏览器失败: ${err.message}`); + }); + } + } +} + +main(); diff --git a/package.json b/package.json index 91b169d..ca37842 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "main": "main.js", "scripts": { "start": "node main.js", - "test": "node main.js" + "test": "node main.js", + "debug:task": "node debugTask.js", + "debug:trending": "TASK=dailyTrending DEBUG_KEEP_OPEN=true node debugTask.js", + "debug:update": "TASK=updateProject DEBUG_KEEP_OPEN=true DEBUG_DRY_RUN=true node debugTask.js" }, "keywords": [ "gitcode", diff --git a/tasks/dailyTrending.js b/tasks/dailyTrending.js index 139a43d..5b985b2 100644 --- a/tasks/dailyTrending.js +++ b/tasks/dailyTrending.js @@ -18,6 +18,7 @@ const { waitForElements, getPageInfo, } = require(path.join(__dirname, '..', 'utils', 'page')); +const { runDebugSteps } = require(path.join(__dirname, '..', 'utils', 'debugSteps')); const PROJECT_CARD_SELECTORS = [ '.hot-selection .recommend-card', @@ -32,44 +33,91 @@ const PROJECT_CARD_SELECTORS = [ * 执行查看热门任务 * @param {import('playwright').Page} page - 主页面 */ -async function dailyTrending(page) { +async function dailyTrending(page, options = {}) { logger.info('👉 执行查看热门'); - // 从主页面找到"查看热门"任务卡片 - const tasks = await page.locator('.novice-task-item-box').all(); - const trendingTask = await findTaskByTitle(tasks, '查看热门'); + const ctx = { + page, + trendingTask: null, + trendingPage: null, + cards: [], + detailPage: null, + }; - if (!trendingTask) { - throw new Error('未找到"查看热门"任务卡片'); - } - - // ── 第一步:打开热门页面 ── - const trendingPage = await openNewPage( - page.context(), - () => trendingTask.locator('.toComplete').click() - ); + const steps = [ + { + name: '查找查看热门任务卡片', + page: (state) => state.page, + run: async (state) => { + const tasks = await state.page.locator('.novice-task-item-box').all(); + state.trendingTask = await findTaskByTitle(tasks, '查看热门'); + if (!state.trendingTask) { + throw new Error('未找到"查看热门"任务卡片'); + } + }, + inspect: async (state) => ({ + taskCardCount: await state.page.locator('.novice-task-item-box').count(), + titleLabelCount: await state.page.locator('.task-card-title__label-text').count(), + toCompleteCount: await state.page.locator('.toComplete').count(), + }), + }, + { + name: '点击去完成并打开热门页', + page: (state) => state.page, + run: async (state) => { + state.trendingPage = await openNewPage( + state.page.context(), + () => state.trendingTask.locator('.toComplete').click() + ); + state.currentPage = state.trendingPage; + }, + inspect: async (state) => ({ + taskToCompleteVisible: await state.trendingTask.locator('.toComplete').isVisible().catch(() => false), + }), + }, + { + name: '查找热门项目卡片', + page: (state) => state.trendingPage, + run: async (state) => { + state.cards = await findProjectCards(state.trendingPage); + }, + inspect: async (state) => ({ + hotSelectionCount: await state.trendingPage.locator('.hot-selection').count(), + recommendCardCount: await state.trendingPage.locator('a.recommend-card').count(), + sourceModuleLinkCount: await state.trendingPage.locator('a[href*="source_module"]').count(), + allLinkCount: await state.trendingPage.locator('a[href]').count(), + }), + }, + { + name: '打开第一个热门项目详情页', + page: (state) => state.trendingPage, + run: async (state) => { + state.detailPage = await openLinkPage( + state.trendingPage.context(), + state.cards[0], + { label: '热门项目详情页' } + ); + state.currentPage = state.detailPage; + }, + inspect: async (state) => ({ + cardCount: state.cards.length, + }), + }, + { + name: '等待热门项目详情页加载', + page: (state) => state.detailPage, + run: async (state) => { + await waitPageReady(state.detailPage); + logger.info('热门项目详情页已打开'); + }, + }, + ]; 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); - } + await runDebugSteps('查看热门', steps, ctx, options); } finally { - await safeClose(trendingPage); + await safeClose(ctx.detailPage); + await safeClose(ctx.trendingPage); } } diff --git a/tasks/updateProject.js b/tasks/updateProject.js index bb4af02..4a6f942 100644 --- a/tasks/updateProject.js +++ b/tasks/updateProject.js @@ -11,59 +11,151 @@ 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')); +const { runDebugSteps } = require(path.join(__dirname, '..', 'utils', 'debugSteps')); /** * 执行更新项目任务 * @param {import('playwright').Page} page - 主页面 */ -async function updateProject(page) { +async function updateProject(page, options = {}) { logger.info('👉 执行项目更新'); - if (!config.urls.testRepo) { - throw new Error('未配置 GITCODE_TEST_REPO_URL,无法执行更新项目任务'); - } + const ctx = { + page, + newPage: null, + frame: null, + content: '', + }; - const context = page.context(); - const newPage = await context.newPage(); + const steps = [ + { + name: '检查 GITCODE_TEST_REPO_URL 配置', + page: (state) => state.page, + run: async () => { + if (!config.urls.testRepo) { + throw new Error('未配置 GITCODE_TEST_REPO_URL,无法执行更新项目任务'); + } + }, + inspect: async () => ({ + testRepoConfigured: Boolean(config.urls.testRepo), + }), + }, + { + name: '打开仓库编辑页', + page: (state) => state.newPage || state.page, + run: async (state) => { + state.newPage = await state.page.context().newPage(); + state.currentPage = state.newPage; + await safeGoto(state.newPage, config.urls.testRepo, { + label: '仓库编辑页', + timeout: config.navigationTimeout, + waitFor: ['#codeartside'], + }); + logger.info('已进入仓库编辑页面'); + }, + inspect: async (state) => ({ + codeartsideCount: state.newPage ? await state.newPage.locator('#codeartside').count() : 0, + }), + }, + { + name: '等待 #codeartside iframe', + page: (state) => state.newPage, + run: async (state) => { + await state.newPage.locator('#codeartside').waitFor({ + state: 'attached', + timeout: 15000, + }); + state.frame = state.newPage.frameLocator('#codeartside'); + }, + inspect: async (state) => ({ + codeartsideCount: await state.newPage.locator('#codeartside').count(), + }), + }, + { + name: '定位 Monaco 编辑器', + page: (state) => state.newPage, + run: async (state) => { + await state.frame.locator('.monaco-editor').click({ + position: { x: 200, y: 100 }, + }); + }, + inspect: async (state) => ({ + iframeCount: await state.newPage.locator('#codeartside').count(), + monacoEditorCount: await state.frame.locator('.monaco-editor').count(), + textareaCount: await state.frame.locator('textarea').count(), + }), + }, + { + name: '移动光标到文件末尾', + page: (state) => state.newPage, + run: async (state) => { + await state.newPage.keyboard.press('Control+End'); + await state.newPage.waitForTimeout(300); + }, + }, + { + name: '输入自动更新内容', + page: (state) => state.newPage, + run: async (state) => { + state.content = `\n# 自动更新 - ${new Date().toISOString()}`; + await state.newPage.keyboard.type(state.content, { delay: 50 }); + logger.info('已写入内容'); + }, + }, + { + name: '点击第一个提交修改按钮', + page: (state) => state.newPage, + run: async (state) => { + if (state.debug.dryRun) { + logger.warn('DEBUG_DRY_RUN=true,跳过点击提交修改按钮'); + return; + } + await state.newPage.locator('button:has-text("提交修改")').first().click(); + await state.newPage.waitForTimeout(1000); + }, + inspect: async (state) => ({ + submitButtonCount: await state.newPage.locator('button:has-text("提交修改")').count(), + }), + }, + { + name: '等待确认弹窗提交按钮', + page: (state) => state.newPage, + run: async (state) => { + if (state.debug.dryRun) { + logger.warn('DEBUG_DRY_RUN=true,跳过等待确认弹窗'); + return; + } + const confirmBtn = state.newPage.locator('button:has-text("提交修改")').nth(1); + await confirmBtn.waitFor({ state: 'visible', timeout: 10000 }); + }, + inspect: async (state) => ({ + submitButtonCount: await state.newPage.locator('button:has-text("提交修改")').count(), + dialogCount: await state.newPage.locator('[role="dialog"], .modal, .el-dialog').count(), + }), + }, + { + name: '点击弹窗里的提交修改', + page: (state) => state.newPage, + run: async (state) => { + if (state.debug.dryRun) { + logger.warn('DEBUG_DRY_RUN=true,跳过最终提交'); + return; + } + const confirmBtn = state.newPage.locator('button:has-text("提交修改")').nth(1); + await confirmBtn.click(); + await state.newPage.waitForTimeout(1000); + logger.info('项目更新已提交'); + }, + inspect: async (state) => ({ + submitButtonCount: await state.newPage.locator('button:has-text("提交修改")').count(), + }), + }, + ]; 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('项目更新已提交'); + await runDebugSteps('更新项目', steps, ctx, options); } finally { - await safeClose(newPage); + await safeClose(ctx.newPage); } } diff --git a/utils/debugSteps.js b/utils/debugSteps.js new file mode 100644 index 0000000..37fb008 --- /dev/null +++ b/utils/debugSteps.js @@ -0,0 +1,173 @@ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); +const logger = require('./logger'); + +function isEnabled(options = {}) { + return options.debugTask === true || process.env.DEBUG_TASK === 'true'; +} + +function shouldKeepOpen(options = {}) { + return options.keepOpen === true || process.env.DEBUG_KEEP_OPEN === 'true'; +} + +function shouldPauseEachStep(options = {}) { + return options.pauseEachStep === true || process.env.DEBUG_PAUSE_EACH_STEP === 'true'; +} + +function isDryRun(options = {}) { + return options.dryRun === true || process.env.DEBUG_DRY_RUN === 'true'; +} + +function createDebugRunDir(taskName) { + const stamp = new Date().toISOString() + .replace(/[:.]/g, '-') + .replace('T', '_') + .slice(0, 19); + const safeName = String(taskName).replace(/[^\w.-]+/g, '_'); + const dir = path.resolve('debug-runs', `${stamp}_${safeName}`); + fs.mkdirSync(dir, { recursive: true }); + return dir; +} + +async function pageInfo(page) { + if (!page || page.isClosed()) { + return { title: '', url: '(page closed)' }; + } + + const title = await page.title().catch(() => ''); + const url = page.url(); + return { title, url }; +} + +async function saveFailureArtifacts(page, dir, index, stepName, err) { + if (!dir || !page || page.isClosed()) return; + + const safeStep = String(stepName).replace(/[^\w.-]+/g, '_').slice(0, 80); + const prefix = `${String(index).padStart(2, '0')}_${safeStep}`; + + await page.screenshot({ + path: path.join(dir, `${prefix}_fail.png`), + fullPage: true, + }).catch((screenshotErr) => { + logger.warn(` → 失败截图保存失败: ${screenshotErr.message}`); + }); + + const html = await page.content().catch(() => ''); + if (html) { + fs.writeFileSync(path.join(dir, `${prefix}_fail.html`), html, 'utf-8'); + } + + fs.writeFileSync( + path.join(dir, `${prefix}_error.txt`), + err.stack || err.message, + 'utf-8' + ); +} + +function formatInspectValue(value) { + if (value === null || typeof value === 'undefined') return String(value); + if (typeof value === 'string') return value; + return JSON.stringify(value); +} + +async function runInspect(step, ctx) { + if (typeof step.inspect !== 'function') return; + + logger.info('元素检查:'); + try { + const result = await step.inspect(ctx); + if (!result || typeof result !== 'object') { + logger.info(` ${formatInspectValue(result)}`); + return; + } + + for (const [key, value] of Object.entries(result)) { + logger.info(` ${key}: ${formatInspectValue(value)}`); + } + } catch (err) { + logger.warn(` inspect 执行失败: ${err.message}`); + } +} + +function waitForEnter(message) { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + rl.question(message, () => { + rl.close(); + resolve(); + }); + }); +} + +async function runDebugSteps(taskName, steps, ctx = {}, options = {}) { + const enabled = isEnabled(options); + const debugOptions = { + enabled, + keepOpen: shouldKeepOpen(options), + pauseEachStep: shouldPauseEachStep(options), + dryRun: isDryRun(options), + runDir: enabled ? createDebugRunDir(taskName) : '', + }; + + ctx.debug = debugOptions; + + if (enabled) { + logger.divider(); + logger.info(`[DEBUG] 单任务调试: ${taskName}`); + logger.info(`[DEBUG] 产物目录: ${debugOptions.runDir}`); + logger.info(`[DEBUG] keepOpen=${debugOptions.keepOpen}, pauseEachStep=${debugOptions.pauseEachStep}, dryRun=${debugOptions.dryRun}`); + } + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const stepNo = i + 1; + const label = `Step ${stepNo}/${steps.length}: ${step.name}`; + + if (enabled) { + logger.info(`[DEBUG] ${label} 开始`); + } else { + logger.info(` → ${step.name}`); + } + + try { + await step.run(ctx); + + if (enabled) { + const info = await pageInfo(step.page ? step.page(ctx) : ctx.page); + logger.info(`[DEBUG] ${label} ✅`); + logger.info(`[DEBUG] 当前页面: ${info.url}`); + } + + if (enabled && debugOptions.pauseEachStep) { + await waitForEnter(`[DEBUG] ${label} 已完成,按 Enter 继续...`); + } + } catch (err) { + logger.error(enabled ? `[DEBUG] ${label} ❌` : `${label} ❌`); + logger.error(`错误: ${err.message}`); + + const activePage = step.page ? step.page(ctx) : (ctx.currentPage || ctx.page); + const info = await pageInfo(activePage); + logger.error(`当前页面: ${info.url}`); + logger.error(`页面标题: ${info.title}`); + + await runInspect(step, ctx); + await saveFailureArtifacts(activePage, debugOptions.runDir, stepNo, step.name, err); + + if (enabled && debugOptions.keepOpen) { + logger.warn('浏览器已保留,请检查页面元素。'); + await waitForEnter('按 Enter 关闭浏览器并退出...'); + } + + throw err; + } + } +} + +module.exports = { + runDebugSteps, + isDryRun, +};