Files
zhaopin/skill/scripts/scoring.js
T
mzl92 e0d9c4e434 初始提交: 智联简历筛选系统v10(含Hermes每日自动化层)
- 硬性预筛门槛: 外贸经验必须(仅工作经历匹配), 培养类拦截, 毕业2020-2026, 年龄<=35
- 七维评分+动态阈值+打招呼纪律
- Hermes每日自动化: daily-runner(09:30-18:00)/daily-report/enforce-cutoff/stop-tasks
- v10.4: 每累计200人自动刷新页面
- 修正: CDP端口9222(反向隧道), 通知只发私聊(防串台), hr2任务禁用
- WSL端: cdp-tunnel.sh 浏览器工人守护
2026-08-26 22:55:08 +08:00

295 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// scoring.js v10 — 简历筛选评分与硬性预筛门槛(纯逻辑,无浏览器依赖,可离线测试)
//
// v10 变更(2026-08-26,需求方反馈"打招呼范围过宽"):
// 1. 新增硬性预筛门槛 hardGate():不相关候选人直接标记"不相关",永不进入打招呼流程
// - 门槛A(岗位/经历):全文须含外贸类关键词(外贸/外贸销售/外贸业务/外贸汽车/外贸助理/
// 外贸主管/外贸经理/外销/国际贸易/国贸/海外销售/出口/跨境电商/二手车出口/汽车出口/整车出口)
// - 门槛B(毕业专业):教育经历须含 英语/商务英语/应用英语/俄语/西班牙语/西语/法语/
// 国际经济与贸易/贸易经济
// - 门槛C(技能证书):须含 英语四级/六级/八级(CET-4/6, 专四/专八)、俄语四/六/八级、
// 法语四级/四八级、西班牙语四八级 等指定证书
// - A/B/C 满足任一即算"方向相关";都不满足 → 不相关,不评分不打招呼
// 2. 新增毕业时间窗口:2020-2026 届(教育经历中解析到的最晚年份;
// 解析不到时不硬拒,gateReason 标记 year-unknown,可配置 GRAD_YEAR.strict=true 改为硬拒)
// 3. 保留 v9 七维评分 + 动态阈值 + 年龄>35 一票否决 + 岗位推荐标签
//
// 用法(脚本内):const { hardGate, scoreCandidate } = require('./scoring');
// 离线测试:node scoring.js # 跑内置样例
// node scoring.js --db <path> # 用真实数据库抽样验证
'use strict';
// ========== 可配置筛选条件(需求方 2026-08-26 更新) ==========
const CONFIG = {
// 门槛A:岗位/经历关键词(任一命中即通过,全文匹配 rawText+workDetail)
tradeKeywords: [
'外贸', '外销', '国际贸易', '国贸', '海外销售', '出口',
'跨境电商', '跨境', '二手车出口', '汽车出口', '整车出口', '海外市场',
],
// 门槛B:毕业专业(教育经历 educationDetail 匹配,rawText 兜底)
majorKeywords: [
'英语', '商务英语', '应用英语', '俄语', '西班牙语', '西语', '法语',
'国际经济与贸易', '贸易经济',
],
// 门槛C:技能证书(certDetail + rawText 匹配)
certKeywords: [
// 英语
'CET-4', 'CET-6', 'CET4', 'CET6',
'大学英语四级', '大学英语六级', '英语四级', '英语六级', '英语八级',
'英语专业四级', '英语专业八级', '专四', '专八', 'TEM-4', 'TEM-8',
// 俄语
'俄语四级', '俄语六级', '俄语八级', '俄语专业四级', '俄语专业八级',
// 法语
'法语四级', '法语六级', '法语八级', '法语四八级', '法语专业四级', '法语专业八级',
// 西班牙语
'西班牙语四级', '西班牙语六级', '西班牙语八级', '西班牙语四八级',
'西语四级', '西语六级', '西语八级', '西语四八级',
],
// 门槛D:毕业时间窗口 [min, max](含边界;教育经历解析不到年份时见 strict)
gradYear: { min: 2020, max: 2026, strict: false },
// 年龄硬否决(v9 沿用)
maxAge: 35,
// 动态阈值(v9 沿用):行业匹配≥4 时推荐线降 2 分
thresholds: { recommend: 26, recommendIndustry: 24, maybe: 18 },
};
// ========== v9 七维评分关键词(与 jd-requirements.md 映射一致) ==========
const INDUSTRY_HIGH = ['二手车', '整车出口', '汽车出口', '车辆出口', '海外汽车', 'VIN', '车架号', '滚装船', '车型配置', '多VIN'];
const INDUSTRY_MID = ['汽配', '工程机械', '大宗', '汽车', '车辆'];
const TRADE_HIGH = ['询盘', '报价', '谈判', '成交', 'PI', '报关', '清关', '收款', '复购', '客户开发', '订单', 'T/T', 'LC']; // 仅 workDetail
const TRADE_MID = ['外贸', '出口', '跨境电商', '海外客户', '国际贸易', '跟单', '外销'];
const LANG_TOP = ['雅思', 'IELTS', '托福', 'TOEFL', 'BEC', '俄语', '法语', '西班牙语', '西语', '葡萄牙语', '阿拉伯语'];
const LANG_MID = ['CET-6', 'CET6', '大学英语六级', '英语六级', '专八', '英语专业八级', 'TEM-8'];
const LANG_LOW = ['CET-4', 'CET4', '大学英语四级', '英语四级', '专四', '英语专业四级', 'TEM-4', '商务英语', '英语口语'];
const CHANNEL_HIGH = ['阿里巴巴国际站', '阿里国际站', '独立站', 'LinkedIn', 'Facebook', 'WhatsApp', 'Google', '展会', '中国制造网', 'Instagram']; // 仅 workDetail
const CHANNEL_LOW = ['客户开发', '邮件开发', '获客', '社媒推广', '社媒'];
const MAJOR_HIGH = ['商务英语', '国际贸易', '英语', '国际经济与贸易'];
const MAJOR_LANG = ['俄语', '西班牙语', '西语', '法语', '德语', '日语', '朝鲜语', '翻译', '应用英语'];
const MAJOR_OTHER = ['市场营销', '跨境电商', '电子商务', '贸易经济'];
function countHits(text, list) {
return list.reduce((n, k) => (text.includes(k) ? n + 1 : n), 0);
}
// ========== 硬性预筛门槛 ==========
// 从教育经历解析毕业年份(取出现的最晚年份作为毕业时间)
function parseGradYear(educationDetail, rawText) {
const src = educationDetail || rawText || '';
const years = (src.match(/(?:19|20)\d{2}/g) || []).map(Number).filter(y => y >= 1990 && y <= 2030);
if (!years.length) return null;
return Math.max(...years);
}
/**
* 硬性预筛:通过返回 { pass: true, reasons: [...] };
* 不通过返回 { pass: false, reason: '...' }(reason 用于日志与数据库 notes)。
* 数据形态与 extractModalData 输出一致:{ rawText, workDetail, educationDetail, certDetail, age }
*/
function hardGate(data) {
const rawText = data.rawText || '';
const workDetail = data.workDetail || '';
const eduDetail = data.educationDetail || '';
const certDetail = data.certDetail || '';
const reasons = [];
// 年龄一票否决(先于其他门槛,v9 沿用)
if (data.age && data.age > CONFIG.maxAge) {
return { pass: false, reason: `age>${CONFIG.maxAge}`, level: '不推荐' };
}
// A:外贸相关【经验】必须命中(2026-08-26 需求方收紧:只要相关经验,不再考虑培养类)
// 只匹配 workDetail(工作经历)——求职意向/教育背景里提到外贸不算经验
const tradeHit = CONFIG.tradeKeywords.filter(k => workDetail.includes(k));
const majorSrc = eduDetail || rawText;
const majorHit = CONFIG.majorKeywords.filter(k => majorSrc.includes(k));
const certSrc = certDetail + '\n' + rawText;
const certHit = CONFIG.certKeywords.filter(k => certSrc.includes(k));
if (!tradeHit.length) {
// B/C 只中专业或证书 = 培养类,明确拦截并注明原因便于统计
const traineeSignal = [];
if (majorHit.length) traineeSignal.push(`major:${majorHit[0]}`);
if (certHit.length) traineeSignal.push(`cert:${certHit[0]}`);
return {
pass: false,
reason: traineeSignal.length ? `trainee-only(${traineeSignal.join('+')})` : 'no-trade-experience',
level: '不相关',
};
}
reasons.push(`经验:${tradeHit.slice(0, 3).join('/')}`);
if (majorHit.length) reasons.push(`专业:${majorHit.slice(0, 3).join('/')}`);
if (certHit.length) reasons.push(`证书:${certHit.slice(0, 3).join('/')}`);
// D:毕业时间窗口
const gradYear = parseGradYear(eduDetail, rawText);
if (gradYear !== null && (gradYear < CONFIG.gradYear.min || gradYear > CONFIG.gradYear.max)) {
return { pass: false, reason: `grad-year-${gradYear}-out-of-${CONFIG.gradYear.min}-${CONFIG.gradYear.max}`, level: '不相关' };
}
if (gradYear === null) reasons.push('year-unknown');
return { pass: true, reasons, gradYear };
}
// ========== 活跃度解析(v9 沿用 v5 逻辑) ==========
function parseActivity(activity) {
if (!activity) return 2;
const text = activity.toLowerCase();
if (/^\d+分钟前/.test(text) && /回复|浏览/.test(text)) return 5;
if (/半小时前|1小时前/.test(text)) return 4;
if (/^\d+小时前/.test(text)) return 4;
if (/48小时前在线/.test(text)) return 3;
if (/^\d天前(有回复|在线|浏览)/.test(text) || /^[123]天前/.test(text)) return 3;
if (/新注册/.test(text)) return 2;
if (/^\d+天前/.test(text)) return 1;
return 2;
}
// ========== 经验年数解析(注意"27年应届生"陷阱,先判应届) ==========
function parseExperienceYears(exp) {
if (!exp) return null;
if (/应届生|在校生/.test(exp)) return 0;
const m = exp.match(/(\d+)年(\d*个月?)?/);
if (m) return parseInt(m[1], 10) + (parseInt((m[2] || '').replace(/[^0-9]/g, ''), 10) || 0) / 12;
return null;
}
// ========== 七维评分(v9 重建版,维度名与数据库 score_details 一致) ==========
function scoreCandidate(data) {
const rawText = data.rawText || '';
const workDetail = data.workDetail || '';
const eduDetail = data.educationDetail || '';
const fullText = rawText + '\n' + workDetail;
const expYears = parseExperienceYears(data.experience || data.positionExp || '') || 0;
// 1. 行业匹配(全文):二手车/汽车出口类高分,汽配/工程机械中分
const indH = countHits(fullText, INDUSTRY_HIGH);
const indM = countHits(fullText, INDUSTRY_MID);
let industryScore = 1;
if (indH >= 2) industryScore = 5;
else if (indH === 1) industryScore = 4;
else if (indM >= 2) industryScore = 3;
else if (indM === 1) industryScore = 2;
// 2. 外贸核心能力:highTrade 仅从 workDetail 匹配(求职意向不算数),midTrade 全文
const highTrade = countHits(workDetail, TRADE_HIGH);
const midTrade = countHits(fullText, TRADE_MID);
let tradeScore = 1;
if (highTrade >= 2) tradeScore = 5;
else if (highTrade === 1) tradeScore = 4;
else if (midTrade >= 2) tradeScore = 3;
else if (midTrade === 1) tradeScore = 2;
// 3. 语言能力:小语种/雅思托福最高;外贸经验强时保底 2 分(语言不一票否决实战派)
const langTop = countHits(fullText, LANG_TOP);
const langMid = countHits(fullText, LANG_MID);
const langLow = countHits(fullText, LANG_LOW);
let langScore = 1;
if (langTop >= 1) langScore = 5;
else if (langMid >= 1) langScore = 4;
else if (langLow >= 1) langScore = 3;
if (tradeScore <= 2 && highTrade >= 2) langScore = Math.max(langScore, 2);
// 4. 客户开发与获客(仅 workDetail,避免实习/意向虚高;区分高/低含金量)
const chH = countHits(workDetail, CHANNEL_HIGH);
const chL = countHits(workDetail, CHANNEL_LOW);
let channelScore = 1;
if (chH >= 2) channelScore = 5;
else if (chH === 1 && chL >= 1) channelScore = 4;
else if (chH === 1) channelScore = 3;
else if (chL >= 2) channelScore = 3;
else if (chL === 1) channelScore = 2;
// 5. 专业背景:高匹配专业+学历综合
const edu = data.education || '';
let eduBase = 1;
if (edu === '硕士' || edu === '博士') eduBase = 5;
else if (edu === '本科') eduBase = 4;
else if (edu === '大专') eduBase = 3;
const majorSrc = eduDetail || rawText;
let majorScore = 1;
if (countHits(majorSrc, MAJOR_HIGH) >= 1 && eduBase >= 3) majorScore = 5;
else if (countHits(majorSrc, MAJOR_LANG) >= 1) majorScore = 4;
else if (countHits(majorSrc, MAJOR_OTHER) >= 1) majorScore = 3;
else if (eduBase >= 4) majorScore = 2;
// 6. 稳定性:最长在职时长 + 工作段数
let stabilityScore = 3;
if (workDetail) {
const dms = workDetail.match(/\((\d+年\s*\d*个月?)\)/g) || workDetail.match(/(\d+年\d*个月?)(?=[\s\n])/g);
if (dms && dms.length) {
const durations = dms.map(d => {
const m = d.match(/(\d+)年\s*(\d*个月?)?/);
return (parseInt(m[1], 10) || 0) + (parseInt((m[2] || '').replace(/[^0-9]/g, ''), 10) || 0) / 12;
});
const mx = Math.max(...durations);
const jc = durations.length;
if (mx >= 2 && jc <= 3) stabilityScore = 5;
else if (mx >= 1.5 && jc <= 4) stabilityScore = 4;
else if (mx >= 1) stabilityScore = 3;
else if (durations[0] < 0.5 && jc > 3) stabilityScore = 1;
else stabilityScore = 2;
}
}
// 7. 活跃度
const activityScore = parseActivity(data.activity || '');
const total = industryScore + tradeScore + langScore + channelScore + majorScore + stabilityScore + activityScore;
// 动态阈值(v9):行业强 → 推荐线降 2 分
const recommendLine = industryScore >= 4 ? CONFIG.thresholds.recommendIndustry : CONFIG.thresholds.recommend;
let level = total >= recommendLine ? '推荐' : (total >= CONFIG.thresholds.maybe ? '待定' : '不推荐');
// 岗位推荐标签(v9)
let roleTag = '不限';
if (expYears >= 1 && (tradeScore >= 3 || channelScore >= 3 || industryScore >= 3)) roleTag = '有经验岗';
else if (expYears < 1 && (langScore >= 3 || majorScore >= 3)) roleTag = '应届生岗';
return {
total, level, roleTag,
details: { industryScore, tradeScore, langScore, channelScore, majorScore, stabilityScore, activityScore },
};
}
// ========== 总入口:门槛 + 评分 ==========
/**
* evaluateCandidate(data) → { gate, score, total, level, details }
* 门槛不通过时 level 为 '不相关'(或年龄否决的 '不推荐'),score 为 null,绝不打招呼。
*/
function evaluateCandidate(data) {
const gate = hardGate(data);
if (!gate.pass) {
return { gate, score: null, total: 0, level: gate.level || '不相关', roleTag: '不限', details: { gateReason: gate.reason } };
}
const sc = scoreCandidate(data);
return { gate, score: sc.total, total: sc.total, level: sc.level, roleTag: sc.roleTag, details: sc.details };
}
module.exports = { CONFIG, hardGate, scoreCandidate, evaluateCandidate, parseGradYear, parseExperienceYears, parseActivity };
// ========== 离线自测(node scoring.js 直接运行时) ==========
if (require.main === module) {
const samples = [
{ name: '外贸业务员(应通过)', data: { age: 27, education: '本科', experience: '3年', rawText: '三年外贸业务员经验,负责海外客户开发、询盘报价,参加展会,阿里巴巴国际站运营', workDetail: 'XX外贸公司 外贸业务员 (2年8个月) 客户开发 询盘 报价', educationDetail: '2018-2022 本科 商务英语', certDetail: '大学英语六级 CET-6' } },
{ name: '证券分析(应拦截)', data: { age: 28, education: '本科', experience: '4年', rawText: '证券公司研究员,负责行业研究与投资分析报告', workDetail: 'XX证券 证券分析师 (3年2个月) 行业研究 金融建模', educationDetail: '2014-2018 本科 金融学', certDetail: '证券从业资格' } },
{ name: '保险顾问(应拦截)', data: { age: 30, education: '大专', experience: '5年', rawText: '保险代理人,客户资源维护与保单销售', workDetail: 'XX人寿 保险顾问 (4年1个月) 保单销售 客户维护', educationDetail: '2013-2016 大专 市场营销', certDetail: '' } },
{ name: '英语应届生(应拦截-培养类)', data: { age: 22, education: '本科', experience: '应届生', rawText: '英语专业应届毕业生,求职外贸方向', workDetail: '', educationDetail: '2022-2026 本科 英语', certDetail: '英语专业四级 专四' } },
{ name: '俄语翻译(应拦截-培养类)', data: { age: 24, education: '本科', experience: '1年', rawText: '俄语翻译,协助外贸邮件沟通', workDetail: 'XX贸易 翻译 (1年0个月)', educationDetail: '2018-2022 本科 俄语', certDetail: '俄语专业四级' } },
{ name: '2019届外贸(应拦-毕业年份)', data: { age: 29, education: '本科', experience: '6年', rawText: '外贸经理,团队管理,海外市场', workDetail: 'XX进出口 外贸经理 (5年2个月) 外贸 出口', educationDetail: '2015-2019 本科 国际经济与贸易', certDetail: '' } },
{ name: '36岁外贸老手(应拦-年龄)', data: { age: 36, education: '本科', experience: '12年', rawText: '外贸总监', workDetail: '外贸 (10年0个月)', educationDetail: '2008-2012 本科 英语', certDetail: '专八' } },
];
for (const s of samples) {
const r = evaluateCandidate(s.data);
const verdict = r.gate.pass ? `${r.level} ${r.total}分 [${r.roleTag}] (${r.gate.reasons.join('; ')})` : `⛔ ${r.level} (${r.gate.reason})`;
console.log(`${s.name} → ${verdict}`);
}
}