UNPKG

zhilian-auto-hi

Version:

智联招聘自动打招呼工具 - 自动化招聘流程的命令行工具

345 lines (344 loc) 14.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.modeService = void 0; exports.confirmRunMode = confirmRunMode; exports.runAutoGreetingMode = runAutoGreetingMode; exports.runBrowseMode = runBrowseMode; exports.executeMode = executeMode; const config_1 = require("./config"); const resume_1 = require("./resume"); const delay_1 = require("../utils/delay"); const index_1 = __importDefault(require("../logger/index")); const graceful_shutdown_1 = __importDefault(require("./graceful-shutdown")); const ora_1 = __importDefault(require("ora")); /** * 模式管理服务 * 实现 ShutdownAware 接口以支持优雅关闭 */ class ModeService { constructor() { this.isExecuting = false; this.shutdownCheckInterval = null; } /** * 检查是否应该停止当前操作 */ shouldStop() { return graceful_shutdown_1.default.isShuttingDown(); } /** * 执行清理操作 */ async cleanup() { index_1.default.cleanupProgress('停止模式执行服务'); this.stopShutdownMonitoring(); this.isExecuting = false; } /** * 启动关闭状态监控 * 使用定时器定期检查关闭状态,避免在代码中频繁调用检查 */ startShutdownMonitoring() { if (this.shutdownCheckInterval) { return; // 已经在监控中 } this.shutdownCheckInterval = setInterval(() => { if (this.shouldStop()) { this.stopShutdownMonitoring(); // 通过抛出异常来中断当前执行流程 throw new Error('操作被优雅关闭中断'); } }, 500); // 每500ms检查一次 } /** * 停止关闭状态监控 */ stopShutdownMonitoring() { if (this.shutdownCheckInterval) { clearInterval(this.shutdownCheckInterval); this.shutdownCheckInterval = null; } } /** * 执行带关闭检查的异步操作 * 这是一个优雅的包装器,避免在业务代码中频繁调用检查 */ async executeWithShutdownCheck(operation, operationName) { // 执行前检查一次 if (this.shouldStop()) { throw new Error('操作被优雅关闭中断'); } try { // 启动监控 this.startShutdownMonitoring(); // 执行操作 const result = await operation(); // 执行后再检查一次 if (this.shouldStop()) { throw new Error('操作被优雅关闭中断'); } return result; } catch (error) { // 如果是关闭中断,记录日志 if (error instanceof Error && error.message === '操作被优雅关闭中断') { if (operationName) { index_1.default.info(`${operationName} 被优雅关闭中断`); } } throw error; } finally { // 停止监控 this.stopShutdownMonitoring(); } } /** * 设置执行状态 */ setExecuting(executing) { this.isExecuting = executing; if (!executing) { this.stopShutdownMonitoring(); } } /** * 获取执行状态 */ isCurrentlyExecuting() { return this.isExecuting; } /** * 简单的关闭状态检查(用于关键节点) */ checkShutdownStatus() { if (this.shouldStop()) { throw new Error('操作被优雅关闭中断'); } } } // 创建模式服务实例 const modeService = new ModeService(); exports.modeService = modeService; // 注册到优雅关闭管理器 graceful_shutdown_1.default.registerCleanupCallback(() => modeService.cleanup()); /** * 显示模式确认信息并等待用户确认 * @param mode 运行模式 */ async function confirmRunMode(mode) { return new Promise((resolve, reject) => { // 检查关闭状态 modeService.checkShutdownStatus(); index_1.default.separator('运行模式确认'); // 显示当前运行模式 index_1.default.highlight(`当前运行模式: ${(0, config_1.getRunModeDescription)(mode)} (${mode})`); // 显示模式说明 console.log('\n模式说明:'); index_1.default.info('• 自动打招呼模式 (auto-greeting): 自动筛选简历并发送招呼消息'); index_1.default.info('• 刷浏览模式 (browse): 浏览简历但不发送消息'); console.log('\n按回车键继续运行,或按 Ctrl+C 退出程序...'); // 设置标准输入监听 process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.setEncoding('utf8'); const onData = (key) => { // 检查关闭状态 if (modeService.shouldStop()) { // 清理监听器 process.stdin.setRawMode(false); process.stdin.pause(); process.stdin.removeListener('data', onData); reject(new Error('操作被优雅关闭中断')); return; } // 检查是否按下回车键 (Enter) if (key === '\r' || key === '\n') { // 清理监听器 process.stdin.setRawMode(false); process.stdin.pause(); process.stdin.removeListener('data', onData); index_1.default.success('用户确认继续运行'); resolve(); } // 检查是否按下 Ctrl+C else if (key === '\u0003') { // 清理监听器 process.stdin.setRawMode(false); process.stdin.pause(); process.stdin.removeListener('data', onData); index_1.default.info('用户取消运行'); process.exit(0); } }; process.stdin.on('data', onData); // 设置超时处理(可选) setTimeout(() => { process.stdin.removeListener('data', onData); reject(new Error('用户确认超时')); }, 300000); // 5分钟超时 }); } /** * 执行自动打招呼模式 * @param page 页面对象 * @param config 配置对象 */ async function runAutoGreetingMode(page, config) { try { modeService.setExecuting(true); await modeService.executeWithShutdownCheck(async () => { index_1.default.separator('自动打招呼模式'); // 导航到目标页面 const navigationSpinner = (0, ora_1.default)(`正在访问: ${config.targetUrl}`).start(); await page.goto(config.targetUrl, { waitUntil: 'domcontentloaded' }); navigationSpinner.succeed('目标页面访问完成'); // 等待页面加载完成 await delay_1.DelayUtils.randomDelay(page, 'pageLoad'); // 查找并点击第一个 recommend-item__inner-content 元素 const elementSpinner = (0, ora_1.default)('正在查找第一个 recommend-item__inner-content 元素...').start(); await page.waitForSelector('.recommend-item__inner-content', { timeout: 10000 }); elementSpinner.text = '元素选择器等待完成'; const firstRecommendItem = page.locator('.recommend-item__inner-content').first(); const count = await page.locator('.recommend-item__inner-content').count(); elementSpinner.succeed(`找到 ${count} 个 recommend-item__inner-content 元素`); // 点击第一个简历 await firstRecommendItem.scrollIntoViewIfNeeded(); await delay_1.DelayUtils.randomDelay(page, 'interaction'); await firstRecommendItem.click(); index_1.default.success('已点击第一个 recommend-item__inner-content 元素'); // 等待页面加载 await delay_1.DelayUtils.randomDelay(page, 'pageLoad'); index_1.default.success('等待简历详情页面加载...'); // 开始自动翻页和筛选处理 const maxPages = config.maxPages || 50; const pageDelay = config.pageDelay || 2000; index_1.default.highlight(`年龄限制: ${config.minAge || '无限制'} - ${config.maxAge || '无限制'} 岁`); if (config.gender) { const genderDesc = config.gender === 'male' ? '仅男性' : '仅女性'; index_1.default.highlight(`性别限制: ${genderDesc}`); } if (config.location && config.location.length > 0) { index_1.default.highlight(`现居地址限制: ${config.location.join(', ')}`); } if (config.ignoreKeyWords && config.ignoreKeyWords.length > 0) { index_1.default.highlight(`忽略关键字: ${config.ignoreKeyWords.join(', ')}`); } await (0, resume_1.processResumeWithPagination)(page, config.keywords, maxPages, pageDelay, config.minAge, config.maxAge, config.gender, config.ignoreKeyWords, config.location); }, '自动打招呼模式'); } catch (error) { if (error instanceof Error && error.message === '操作被优雅关闭中断') { index_1.default.info('自动打招呼模式被优雅关闭中断'); throw error; } throw error; } finally { modeService.setExecuting(false); } } /** * 执行刷浏览模式 * @param page 页面对象 * @param config 配置对象 */ async function runBrowseMode(page, config) { try { modeService.setExecuting(true); await modeService.executeWithShutdownCheck(async () => { index_1.default.separator('刷浏览模式'); // 导航到目标页面 const browseNavigationSpinner = (0, ora_1.default)(`正在访问: ${config.targetUrl}`).start(); await page.goto(config.targetUrl, { waitUntil: 'domcontentloaded' }); browseNavigationSpinner.succeed('目标页面访问完成'); // 等待页面加载完成 await delay_1.DelayUtils.randomDelay(page, 'pageLoad'); // 查找并点击第一个 recommend-item__inner-content 元素 const browseElementSpinner = (0, ora_1.default)('正在查找第一个 recommend-item__inner-content 元素...').start(); await page.waitForSelector('.recommend-item__inner-content', { timeout: 10000 }); browseElementSpinner.text = '元素选择器等待完成'; const firstRecommendItem = page.locator('.recommend-item__inner-content').first(); const count = await page.locator('.recommend-item__inner-content').count(); browseElementSpinner.succeed(`找到 ${count} 个 recommend-item__inner-content 元素`); // 点击第一个简历 await firstRecommendItem.scrollIntoViewIfNeeded(); await delay_1.DelayUtils.randomDelay(page, 'interaction'); await firstRecommendItem.click(); index_1.default.success('已点击第一个 recommend-item__inner-content 元素'); // 等待页面加载 await delay_1.DelayUtils.randomDelay(page, 'pageLoad'); index_1.default.success('等待简历详情页面加载...'); // 开始浏览翻页处理 const maxPages = config.maxPages || 50; const pageDelay = config.pageDelay || 2000; // 显示筛选配置信息 if (config.keywords && config.keywords.length > 0) { index_1.default.info('开始带筛选的浏览模式,将执行筛选检查但不打招呼'); index_1.default.highlight(`筛选关键字: ${config.keywords.join(', ')}`); index_1.default.highlight(`年龄限制: ${config.minAge || '无限制'} - ${config.maxAge || '无限制'} 岁`); if (config.gender) { const genderDesc = config.gender === 'male' ? '仅男性' : '仅女性'; index_1.default.highlight(`性别限制: ${genderDesc}`); } if (config.location && config.location.length > 0) { index_1.default.highlight(`现居地址限制: ${config.location.join(', ')}`); } if (config.ignoreKeyWords && config.ignoreKeyWords.length > 0) { index_1.default.highlight(`忽略关键字: ${config.ignoreKeyWords.join(', ')}`); } } else { index_1.default.info('开始纯浏览模式,不执行筛选和打招呼操作'); } await (0, resume_1.processBrowseWithPagination)(page, maxPages, pageDelay, config.keywords, config.minAge, config.maxAge, config.gender, config.ignoreKeyWords, config.location); }, '刷浏览模式'); } catch (error) { if (error instanceof Error && error.message === '操作被优雅关闭中断') { index_1.default.info('刷浏览模式被优雅关闭中断'); throw error; } throw error; } finally { modeService.setExecuting(false); } } /** * 根据配置的模式执行相应的逻辑 * @param page 页面对象 * @param config 配置对象 */ async function executeMode(page, config) { try { // 初始检查关闭状态 modeService.checkShutdownStatus(); const mode = config.runMode || 'auto-greeting'; index_1.default.info(`准备执行模式: ${(0, config_1.getRunModeDescription)(mode)}`); // 执行对应的模式(每个模式内部已经有关闭检查机制) switch (mode) { case 'auto-greeting': await runAutoGreetingMode(page, config); break; case 'browse': await runBrowseMode(page, config); break; default: index_1.default.error(`未知的运行模式: ${mode}`); throw new Error(`未知的运行模式: ${mode}`); } index_1.default.success(`模式执行完成: ${(0, config_1.getRunModeDescription)(mode)}`); } catch (error) { if (error instanceof Error && error.message === '操作被优雅关闭中断') { index_1.default.info('模式执行被优雅关闭中断'); throw error; } index_1.default.error(`模式执行失败: ${error}`); throw error; } }