zhilian-auto-hi
Version:
智联招聘自动打招呼工具 - 自动化招聘流程的命令行工具
345 lines (344 loc) • 14.7 kB
JavaScript
;
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;
}
}