zhilian-auto-hi
Version:
智联招聘自动打招呼工具 - 自动化招聘流程的命令行工具
404 lines (403 loc) • 18.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resumeProcessingService = void 0;
exports.readResumeDetailsWithResult = readResumeDetailsWithResult;
exports.processResumeWithPagination = processResumeWithPagination;
exports.processBrowseWithPagination = processBrowseWithPagination;
const age_1 = require("../utils/age");
const keyword_1 = require("../utils/keyword");
const gender_1 = require("../utils/gender");
const automation_1 = require("./automation");
const delay_1 = require("../utils/delay");
const index_1 = __importDefault(require("../logger/index"));
const graceful_shutdown_1 = __importDefault(require("./graceful-shutdown"));
/**
* 简历处理服务
* 实现 ShutdownAware 接口,支持优雅关闭
*/
class ResumeProcessingService {
/**
* 检查是否应该停止当前操作
* @returns 是否应该停止
*/
shouldStop() {
return graceful_shutdown_1.default.isShuttingDown();
}
/**
* 执行清理操作
*/
async cleanup() {
index_1.default.cleanupProgress('简历处理服务清理完成');
}
}
// 创建服务实例
const resumeProcessingService = new ResumeProcessingService();
exports.resumeProcessingService = resumeProcessingService;
/**
* 根据pageDelay参数创建动态延迟配置
* @param pageDelay 基准延迟时间(毫秒)
* @returns 延迟配置对象
*/
function createDynamicDelayConfig(pageDelay) {
// 确保pageDelay在合理范围内
const baseDelay = Math.max(500, Math.min(8000, pageDelay));
// 根据延迟时间调整变化范围
let variance;
if (baseDelay < 1000) {
variance = 0.5; // 短延迟使用较大变化范围
}
else if (baseDelay < 3000) {
variance = 0.4; // 中等延迟使用中等变化范围
}
else {
variance = 0.3; // 长延迟使用较小变化范围
}
// 计算最小和最大延迟时间
const varianceAmount = baseDelay * variance;
const minDelay = Math.max(300, Math.round(baseDelay - varianceAmount));
const maxDelay = Math.min(10000, Math.round(baseDelay + varianceAmount));
return {
base: baseDelay,
variance: variance,
min: minDelay,
max: maxDelay
};
}
/**
* 读取简历详情信息并返回筛选结果
* @param page Playwright页面对象
* @param keywords 关键字数组
* @param minAge 最小年龄限制
* @param maxAge 最大年龄限制
* @param genderFilter 性别筛选条件
* @param ignoreKeyWords 忽略关键字数组
* @returns 筛选结果
*/
async function readResumeDetailsWithResult(page, keywords, minAge, maxAge, genderFilter, ignoreKeyWords) {
try {
index_1.default.info('正在查找简历详情页面元素...');
await page.waitForSelector('.new-resume-detail--inner', { timeout: 10000 });
const resumeDetailElement = page.locator('.new-resume-detail--inner');
const resumeText = await resumeDetailElement.textContent() || '';
index_1.default.resumeDetails(resumeText);
// 提取姓名和性别
let extractedGender = (0, gender_1.extractGender)('');
let candidateName = '未知';
try {
// 查找姓名元素
const nameElement = page.locator('.resume-basic-new__name');
const nameElementCount = await nameElement.count();
if (nameElementCount > 0) {
candidateName = await nameElement.textContent() || '未知';
extractedGender = (0, gender_1.extractGender)(candidateName);
index_1.default.info(`提取的姓名: ${candidateName}`, '👤');
index_1.default.info(`提取的性别: ${extractedGender === 'male' ? '男性' : extractedGender === 'female' ? '女性' : '未知'}`, '⚧️');
}
else {
index_1.default.warning('未找到姓名元素 .resume-basic-new__name');
}
}
catch (nameError) {
index_1.default.warning(`提取姓名时出错: ${nameError}`);
}
// 性别筛选
const genderMatchResult = (0, gender_1.checkGenderMatch)(extractedGender, genderFilter);
if (!genderMatchResult.isMatch) {
const filterDesc = genderFilter === 'male' ? '仅男性' : genderFilter === 'female' ? '仅女性' : '不限制';
const genderDesc = extractedGender === 'male' ? '男性' : extractedGender === 'female' ? '女性' : '未知';
index_1.default.error(`性别筛选: 不符合要求 (要求: ${filterDesc}, 候选人: ${genderDesc})`);
return {
isMatch: false,
matchedKeywords: [],
age: undefined,
gender: extractedGender
};
}
if (genderFilter) {
const filterDesc = genderFilter === 'male' ? '仅男性' : '仅女性';
index_1.default.success(`性别筛选: 符合要求 (要求: ${filterDesc})`);
}
// 提取年龄
const extractedAge = (0, age_1.extractAge)(resumeText);
index_1.default.info(`提取的年龄: ${extractedAge !== null ? extractedAge + '岁' : '未找到'}`, '📅');
// 年龄筛选
const ageMatch = (0, age_1.checkAgeMatch)(extractedAge, minAge, maxAge);
if (!ageMatch) {
index_1.default.error(`年龄筛选: 不符合要求 (要求: ${minAge || '无限制'}-${maxAge || '无限制'}岁)`);
return {
isMatch: false,
matchedKeywords: [],
age: extractedAge || undefined,
gender: extractedGender
};
}
index_1.default.success(`年龄筛选: 符合要求 (要求: ${minAge || '无限制'}-${maxAge || '无限制'}岁)`);
// 关键字匹配
const matchResult = (0, keyword_1.checkKeywordMatch)(resumeText, keywords);
// 如果关键字匹配成功,再检查忽略关键字
if (matchResult.isMatch && ignoreKeyWords && ignoreKeyWords.length > 0) {
const ignoreMatchResult = (0, keyword_1.checkIgnoreKeywordMatch)(resumeText, ignoreKeyWords);
if (ignoreMatchResult.isMatch) {
index_1.default.error(`忽略关键字筛选: 包含忽略关键字 [${ignoreMatchResult.matchedKeywords.join(', ')}]`);
return {
isMatch: false,
matchedKeywords: matchResult.matchedKeywords,
age: extractedAge || undefined,
gender: extractedGender
};
}
index_1.default.success(`忽略关键字筛选: 未包含忽略关键字`);
}
// 显示筛选结果
index_1.default.resumeResult(matchResult.isMatch, matchResult.matchedKeywords, extractedAge || undefined, extractedGender);
return {
isMatch: matchResult.isMatch,
matchedKeywords: matchResult.matchedKeywords,
age: extractedAge || undefined,
gender: extractedGender
};
}
catch (error) {
index_1.default.error(`读取简历详情时出错: ${error}`);
return { isMatch: false, matchedKeywords: [], age: undefined, gender: 'unknown' };
}
}
/**
* 自动翻页和筛选处理函数
* @param page Playwright页面对象
* @param keywords 关键字数组
* @param maxPages 最大处理页数
* @param pageDelay 翻页延迟时间
* @param minAge 最小年龄限制
* @param maxAge 最大年龄限制
* @param genderFilter 性别筛选条件
* @param ignoreKeyWords 忽略关键字数组
* @returns 处理统计信息
*/
async function processResumeWithPagination(page, keywords, maxPages = 50, pageDelay = 2000, minAge, maxAge, genderFilter, ignoreKeyWords) {
const stats = {
totalProcessed: 0,
matchedCount: 0,
greetedCount: 0,
currentPage: 1,
isCompleted: false
};
index_1.default.title('开始自动翻页和筛选处理');
for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
// 检查是否需要停止处理
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止简历处理...', '🛑');
stats.shutdownReason = '用户中断';
stats.isCompleted = false;
break;
}
index_1.default.pageInfo(pageNum);
stats.currentPage = pageNum;
try {
// 在读取简历详情前再次检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止简历处理...', '🛑');
stats.shutdownReason = '用户中断';
stats.isCompleted = false;
break;
}
const result = await readResumeDetailsWithResult(page, keywords, minAge, maxAge, genderFilter, ignoreKeyWords);
stats.totalProcessed++;
if (result.isMatch) {
stats.matchedCount++;
index_1.default.highlight('简历匹配成功,开始自动打招呼...', '🤖');
// 在打招呼前检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止简历处理...', '🛑');
stats.shutdownReason = '用户中断';
stats.isCompleted = false;
break;
}
// 打招呼
const greetSuccess = await (0, automation_1.autoGreet)(page);
if (greetSuccess) {
stats.greetedCount++;
index_1.default.success('自动打招呼成功!', '🎉');
}
}
else {
index_1.default.info('简历不匹配,跳过打招呼', '📝');
}
// 在延迟前检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止简历处理...', '🛑');
stats.shutdownReason = '用户中断';
stats.isCompleted = false;
break;
}
// 翻页延迟 - 使用基于pageDelay的随机延迟
const delayConfig = createDynamicDelayConfig(pageDelay);
index_1.default.info(`翻页延迟配置: ${delay_1.DelayUtils.getConfigDescription(delayConfig)}`, '⏱️');
await delay_1.DelayUtils.customDelay(page, delayConfig);
// 在翻页前检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止简历处理...', '🛑');
stats.shutdownReason = '用户中断';
stats.isCompleted = false;
break;
}
// 翻页
index_1.default.info('尝试翻到下一页...', '📖');
await (0, automation_1.clickNextPageButton)(page);
index_1.default.success(`第 ${pageNum} 页处理完成`);
}
catch (error) {
index_1.default.error(`处理第 ${pageNum} 页时出错: ${error}`);
// 即使出错也检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止简历处理...', '🛑');
stats.shutdownReason = '用户中断';
stats.isCompleted = false;
break;
}
try {
await (0, automation_1.clickNextPageButton)(page);
}
catch (nextPageError) {
index_1.default.error(`翻页操作失败: ${nextPageError}`);
}
}
}
// 如果没有被中断,标记处理完成
if (!stats.shutdownReason) {
stats.isCompleted = true;
}
// 输出最终统计信息
index_1.default.title('翻页处理完成!');
index_1.default.stats(stats);
return stats;
}
/**
* 纯浏览翻页处理函数 - 用于刷浏览模式
* @param page Playwright页面对象
* @param maxPages 最大处理页数
* @param pageDelay 翻页延迟时间
* @param keywords 关键字数组(可选,用于筛选)
* @param minAge 最小年龄限制(可选,用于筛选)
* @param maxAge 最大年龄限制(可选,用于筛选)
* @param genderFilter 性别筛选条件(可选,用于筛选)
* @param ignoreKeyWords 忽略关键字数组(可选,用于筛选)
* @returns 浏览统计信息
*/
async function processBrowseWithPagination(page, maxPages = 50, pageDelay = 2000, keywords, minAge, maxAge, genderFilter, ignoreKeyWords) {
const stats = {
totalBrowsed: 0,
currentPage: 1,
startTime: new Date(),
totalFiltered: 0,
matchedCount: 0
};
// 判断是否启用筛选功能
const enableFiltering = keywords && keywords.length > 0;
if (enableFiltering) {
index_1.default.title('开始带筛选的浏览模式翻页处理');
index_1.default.info('将对每个简历执行筛选检查,但不执行打招呼操作', '🔍');
}
else {
index_1.default.title('开始纯浏览模式翻页处理');
}
for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
// 检查是否需要停止浏览
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止浏览模式...', '🛑');
break;
}
index_1.default.pageInfo(pageNum);
stats.currentPage = pageNum;
try {
// 在等待页面加载前检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止浏览模式...', '🛑');
break;
}
// 等待页面加载完成
index_1.default.info('等待页面加载完成...');
await page.waitForSelector('.new-resume-detail--inner', { timeout: 10000 });
// 增加浏览计数
stats.totalBrowsed++;
// 如果启用筛选功能,执行筛选检查
if (enableFiltering && keywords) {
try {
const filterResult = await readResumeDetailsWithResult(page, keywords, minAge, maxAge, genderFilter, ignoreKeyWords);
stats.totalFiltered++;
if (filterResult.isMatch) {
stats.matchedCount++;
}
// 使用专门的浏览模式筛选结果显示函数
index_1.default.browseFilterResult(pageNum, filterResult.isMatch, filterResult.matchedKeywords, filterResult.age, filterResult.gender);
}
catch (filterError) {
index_1.default.warning(`第 ${pageNum} 页筛选检查出错: ${filterError}`);
stats.totalFiltered++;
}
}
else {
index_1.default.info(`已浏览第 ${pageNum} 页`, '👀');
}
// 在延迟前检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止浏览模式...', '🛑');
break;
}
// 翻页延迟 - 使用基于pageDelay的随机延迟
const delayConfig = createDynamicDelayConfig(pageDelay);
index_1.default.info(`浏览模式翻页延迟配置: ${delay_1.DelayUtils.getConfigDescription(delayConfig)}`, '⏱️');
await delay_1.DelayUtils.customDelay(page, delayConfig);
// 在翻页前检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止浏览模式...', '🛑');
break;
}
// 翻页操作
index_1.default.info('尝试翻到下一页...', '📖');
await (0, automation_1.clickNextPageButton)(page);
index_1.default.success(`第 ${pageNum} 页浏览完成`);
}
catch (error) {
index_1.default.error(`浏览第 ${pageNum} 页时出错: ${error}`);
// 即使出错也检查关闭状态
if (resumeProcessingService.shouldStop()) {
index_1.default.info('检测到关闭信号,停止浏览模式...', '🛑');
break;
}
// 即使出错也尝试翻页继续
try {
await (0, automation_1.clickNextPageButton)(page);
}
catch (nextPageError) {
index_1.default.error(`翻页操作失败: ${nextPageError}`);
// 如果翻页也失败,继续下一次循环
}
}
}
// 设置结束时间
stats.endTime = new Date();
// 输出最终浏览统计信息
if (enableFiltering) {
index_1.default.title('带筛选的浏览模式处理完成!');
index_1.default.info(`总浏览页面数: ${stats.totalBrowsed}`, '📊');
index_1.default.info(`总筛选简历数: ${stats.totalFiltered}`, '🔍');
index_1.default.info(`匹配简历数: ${stats.matchedCount}`, '✅');
if (stats.totalFiltered > 0) {
const matchRate = ((stats.matchedCount / stats.totalFiltered) * 100).toFixed(1);
index_1.default.info(`匹配率: ${matchRate}%`, '📈');
}
}
else {
index_1.default.title('纯浏览模式处理完成!');
index_1.default.info(`总浏览页面数: ${stats.totalBrowsed}`, '📊');
}
index_1.default.info(`开始时间: ${stats.startTime.toLocaleString()}`, '⏰');
index_1.default.info(`结束时间: ${stats.endTime.toLocaleString()}`, '⏰');
const duration = stats.endTime.getTime() - stats.startTime.getTime();
index_1.default.info(`总耗时: ${Math.round(duration / 1000)} 秒`, '⏱️');
return stats;
}