1
This commit is contained in:
@@ -341,6 +341,31 @@ class JobManager {
|
||||
resumeInfo: resumeData
|
||||
});
|
||||
|
||||
// 未通过规则/评分的待投递记录标记为 filtered,避免长期 pending
|
||||
const passedIds = new Set(matchedJobs.map((j) => j.id).filter((id) => id != null));
|
||||
const notPassedIds = searchedJobs
|
||||
.map((row) => (row.toJSON ? row.toJSON() : row))
|
||||
.map((j) => j.id)
|
||||
.filter((id) => id != null && !passedIds.has(id));
|
||||
if (notPassedIds.length > 0) {
|
||||
try {
|
||||
await job_postings.update(
|
||||
{ applyStatus: 'filtered' },
|
||||
{
|
||||
where: {
|
||||
id: { [Op.in]: notPassedIds },
|
||||
sn_code,
|
||||
platform,
|
||||
applyStatus: 'pending'
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(`[工作管理] 搜索并投递:不符合条件已标记 filtered ${notPassedIds.length} 条`);
|
||||
} catch (e) {
|
||||
console.warn('[工作管理] 标记 filtered 失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 限制投递数量
|
||||
const jobsToDeliver = matchedJobs.slice(0, maxCount);
|
||||
console.log(`[工作管理] 匹配到 ${matchedJobs.length} 个职位,将投递 ${jobsToDeliver.length} 个`);
|
||||
|
||||
@@ -30,7 +30,7 @@ class DeliverHandler extends BaseHandler {
|
||||
*/
|
||||
async doDeliver(task) {
|
||||
const { sn_code, taskParams } = task;
|
||||
const { keyword, platform = 'boss', pageCount = 3, maxCount = 10, filterRules = {} } = taskParams;
|
||||
const { keyword, platform = 'boss', pageCount = 3, maxCount = 10 } = taskParams;
|
||||
|
||||
console.log(`[自动投递] 开始 - 设备: ${sn_code}, 关键词: ${keyword}`);
|
||||
|
||||
@@ -103,8 +103,8 @@ class DeliverHandler extends BaseHandler {
|
||||
};
|
||||
}
|
||||
|
||||
// 8. 合并过滤配置
|
||||
const filterConfig = this.mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig);
|
||||
// 8. 过滤配置仅来自职位类型 job_types(排除词 / 标题须含词等),不与账号投递配置、任务参数混用
|
||||
const filterConfig = this.mergeFilterConfig(jobTypeConfig);
|
||||
|
||||
// 9. 过滤已投递的公司(repeat_deliver_days 由投递配置给出,缺省 30,上限 365)
|
||||
const repeatDeliverDays = Math.min(365, Math.max(1, Number(deliverConfig.repeat_deliver_days) || 30));
|
||||
@@ -120,6 +120,9 @@ class DeliverHandler extends BaseHandler {
|
||||
recentCompanies
|
||||
);
|
||||
|
||||
// 本轮未进入「可投递」列表的待投递记录,标记为已过滤,避免长期停留在 pending
|
||||
await this.markFilteredJobsNotPassed(pendingJobs, filteredJobs, sn_code, platform);
|
||||
|
||||
const jobsToDeliver = filteredJobs.slice(0, actualMaxCount);
|
||||
|
||||
console.log(`[自动投递] 职位筛选完成 - 原始: ${pendingJobs.length}, 符合条件: ${filteredJobs.length}, 将投递: ${jobsToDeliver.length}`);
|
||||
@@ -294,6 +297,44 @@ class DeliverHandler extends BaseHandler {
|
||||
await command.executeCommands(taskId, [getJobListCommand], this.mqttClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本批中未通过过滤/评分的职位从 pending 更新为 filtered(仍 pending 的仅为通过筛选且等待下轮投递的)
|
||||
* @param {Array} pendingJobs - 本批拉取的待投递
|
||||
* @param {Array} filteredJobs - filterAndScoreJobsForDeliver 通过的结果(含 matchScore)
|
||||
*/
|
||||
async markFilteredJobsNotPassed(pendingJobs, filteredJobs, sn_code, platform) {
|
||||
if (!pendingJobs || pendingJobs.length === 0) {
|
||||
return;
|
||||
}
|
||||
const passedIds = new Set(
|
||||
(filteredJobs || []).map((j) => j.id).filter((id) => id != null)
|
||||
);
|
||||
const notPassedIds = pendingJobs
|
||||
.map((j) => (j && j.id != null ? j.id : null))
|
||||
.filter((id) => id != null && !passedIds.has(id));
|
||||
if (notPassedIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const job_postings = db.getModel('job_postings');
|
||||
const { op } = db.models;
|
||||
try {
|
||||
const [n] = await job_postings.update(
|
||||
{ applyStatus: 'filtered' },
|
||||
{
|
||||
where: {
|
||||
id: { [op.in]: notPassedIds },
|
||||
sn_code,
|
||||
platform,
|
||||
applyStatus: 'pending'
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(`[自动投递] 不符合条件已标记 filtered: ${notPassedIds.length} 条(更新行数 ${n})`);
|
||||
} catch (e) {
|
||||
console.warn('[自动投递] 标记 filtered 失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待投递职位
|
||||
*/
|
||||
@@ -314,42 +355,42 @@ class DeliverHandler extends BaseHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并过滤配置
|
||||
* 自动投递过滤配置:仅使用 job_types(excludeKeywords、titleIncludeKeywords)
|
||||
* 薪资筛选不在此合并(min/max 为 0 表示不做薪资过滤);评分权重仍走 accountConfig.is_salary_priority
|
||||
*/
|
||||
mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig) {
|
||||
// 排除关键词
|
||||
const rawJobTypeExclude = jobTypeConfig?.excludeKeywords
|
||||
? ConfigManager.parseConfig(jobTypeConfig.excludeKeywords, [])
|
||||
: [];
|
||||
mergeFilterConfig(jobTypeConfig) {
|
||||
const base = {
|
||||
exclude_keywords: [],
|
||||
filter_keywords: [],
|
||||
title_include_keywords: [],
|
||||
min_salary: 0,
|
||||
max_salary: 0,
|
||||
priority_weights: []
|
||||
};
|
||||
|
||||
const jobTypeExclude = Array.isArray(rawJobTypeExclude) ? rawJobTypeExclude : [];
|
||||
if (!jobTypeConfig) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const deliverExcludeRaw = ConfigManager.getExcludeKeywords(deliverConfig);
|
||||
const deliverExclude = Array.isArray(deliverExcludeRaw) ? deliverExcludeRaw : [];
|
||||
const filterExcludeRaw = filterRules.excludeKeywords || [];
|
||||
const filterExclude = Array.isArray(filterExcludeRaw) ? filterExcludeRaw : [];
|
||||
if (jobTypeConfig.excludeKeywords) {
|
||||
try {
|
||||
const raw = jobTypeConfig.excludeKeywords;
|
||||
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
base.exclude_keywords = Array.isArray(parsed) ? parsed.map((k) => String(k || '').trim()).filter(Boolean) : [];
|
||||
} catch (e) {
|
||||
base.exclude_keywords = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤关键词
|
||||
const deliverFilterRaw = ConfigManager.getFilterKeywords(deliverConfig);
|
||||
const deliverFilter = Array.isArray(deliverFilterRaw) ? deliverFilterRaw : [];
|
||||
const filterKeywordsRaw = filterRules.keywords || [];
|
||||
const filterKeywords = Array.isArray(filterKeywordsRaw) ? filterKeywordsRaw : [];
|
||||
|
||||
// 薪资范围
|
||||
const salaryRange = filterRules.minSalary || filterRules.maxSalary
|
||||
? { min: filterRules.minSalary || 0, max: filterRules.maxSalary || 0 }
|
||||
: ConfigManager.getSalaryRange(deliverConfig);
|
||||
|
||||
let title_include_keywords = [];
|
||||
if (jobTypeConfig && jobTypeConfig.titleIncludeKeywords != null) {
|
||||
if (jobTypeConfig.titleIncludeKeywords != null) {
|
||||
const v = jobTypeConfig.titleIncludeKeywords;
|
||||
if (Array.isArray(v)) {
|
||||
title_include_keywords = v.map((k) => String(k || '').trim()).filter(Boolean);
|
||||
base.title_include_keywords = v.map((k) => String(k || '').trim()).filter(Boolean);
|
||||
} else if (typeof v === 'string' && v.trim()) {
|
||||
try {
|
||||
const p = JSON.parse(v);
|
||||
if (Array.isArray(p)) {
|
||||
title_include_keywords = p.map((k) => String(k || '').trim()).filter(Boolean);
|
||||
base.title_include_keywords = p.map((k) => String(k || '').trim()).filter(Boolean);
|
||||
}
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
@@ -357,14 +398,7 @@ class DeliverHandler extends BaseHandler {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exclude_keywords: [...jobTypeExclude, ...deliverExclude, ...filterExclude],
|
||||
filter_keywords: filterKeywords.length > 0 ? filterKeywords : deliverFilter,
|
||||
title_include_keywords,
|
||||
min_salary: salaryRange.min,
|
||||
max_salary: salaryRange.max,
|
||||
priority_weights: ConfigManager.getPriorityWeights(deliverConfig)
|
||||
};
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,19 @@ const db = require('../../dbProxy');
|
||||
* 自动投递只需调 filterAndScoreJobsForDeliver 一个方法。
|
||||
*/
|
||||
class JobFilterEngine {
|
||||
getJobKey(job) {
|
||||
return String(job.id || job.jobId || `${job.companyName || ''}|${job.jobTitle || ''}`);
|
||||
}
|
||||
|
||||
getRemovedTitles(beforeJobs, afterJobs, limit = 5) {
|
||||
const keptKeySet = new Set(afterJobs.map((job) => this.getJobKey(job)));
|
||||
return beforeJobs
|
||||
.filter((job) => !keptKeySet.has(this.getJobKey(job)))
|
||||
.map((job) => job.jobTitle || '')
|
||||
.filter(Boolean)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤职位列表(薪资 → 标题须含词 → 关键词 → 活跃度 → 去重)
|
||||
* @param {Array} jobs - 职位列表
|
||||
@@ -23,50 +36,56 @@ class JobFilterEngine {
|
||||
let filtered = [...jobs];
|
||||
|
||||
// 1. 薪资过滤
|
||||
const beforeSalary = filtered.length;
|
||||
const beforeSalaryJobs = [...filtered];
|
||||
filtered = this.filterBySalary(filtered, config);
|
||||
const salaryRemoved = beforeSalary - filtered.length;
|
||||
const salaryRemoved = beforeSalaryJobs.length - filtered.length;
|
||||
if (salaryRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤1-薪资过滤: 输入${beforeSalary} 输出${filtered.length} 剔除${salaryRemoved} (范围: ${config.min_salary ?? 0}-${config.max_salary ?? 0}K)`);
|
||||
const removedTitles = this.getRemovedTitles(beforeSalaryJobs, filtered);
|
||||
console.log(`[jobFilterEngine] 步骤1-薪资过滤: 范围=${config.min_salary ?? 0}-${config.max_salary ?? 0}K 剔除标题=${removedTitles.join(' | ') || '无'}`);
|
||||
}
|
||||
|
||||
// 2. 职位标题须包含(job_types.titleIncludeKeywords,仅 jobTitle/jobName/name,与 commonSkills 无关)
|
||||
const beforeTitleKw = filtered.length;
|
||||
// 2. 职位标题须包含(job_types.titleIncludeKeywords,仅 jobTitle,与 commonSkills 无关)
|
||||
const beforeTitleFilterJobs = [...filtered];
|
||||
filtered = this.filterByTitleIncludeKeywords(filtered, config);
|
||||
const titleKwRemoved = beforeTitleKw - filtered.length;
|
||||
const titleKwRemoved = beforeTitleFilterJobs.length - filtered.length;
|
||||
if (titleKwRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤2-标题须含: 输入${beforeTitleKw} 输出${filtered.length} 剔除${titleKwRemoved} (须同时含: ${(config.title_include_keywords || []).join(' · ') || '无'})`);
|
||||
const removedTitles = this.getRemovedTitles(beforeTitleFilterJobs, filtered);
|
||||
console.log(`[jobFilterEngine] 步骤2-标题须含: 关键词=[${(config.title_include_keywords || []).join('、') || '无'}] 剔除标题=${removedTitles.join(' | ') || '无'}`);
|
||||
}
|
||||
|
||||
// 3. 关键词过滤(排除词 + filter_keywords,匹配标题与行业等)
|
||||
const beforeKeywords = filtered.length;
|
||||
const beforeKeywordFilterJobs = [...filtered];
|
||||
filtered = this.filterByKeywords(filtered, config);
|
||||
const keywordsRemoved = beforeKeywords - filtered.length;
|
||||
const keywordsRemoved = beforeKeywordFilterJobs.length - filtered.length;
|
||||
if (keywordsRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤3-关键词过滤: 输入${beforeKeywords} 输出${filtered.length} 剔除${keywordsRemoved} (排除: ${(config.exclude_keywords || []).join(',') || '无'} 包含: ${(config.filter_keywords || []).join(',') || '无'})`);
|
||||
const removedTitles = this.getRemovedTitles(beforeKeywordFilterJobs, filtered);
|
||||
console.log(`[jobFilterEngine] 步骤3-关键词过滤: 排除=[${(config.exclude_keywords || []).join('、') || '无'}] 包含=[${(config.filter_keywords || []).join('、') || '无'}] 剔除标题=${removedTitles.join(' | ') || '无'}`);
|
||||
}
|
||||
|
||||
// 4. 公司活跃度过滤
|
||||
if (config.filter_inactive_companies) {
|
||||
const beforeActivity = filtered.length;
|
||||
const beforeActivityJobs = [...filtered];
|
||||
filtered = await this.filterByCompanyActivity(filtered, config.company_active_days || 7);
|
||||
const activityRemoved = beforeActivity - filtered.length;
|
||||
const activityRemoved = beforeActivityJobs.length - filtered.length;
|
||||
if (activityRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤4-公司活跃度过滤: 输入${beforeActivity} 输出${filtered.length} 剔除${activityRemoved}`);
|
||||
const removedTitles = this.getRemovedTitles(beforeActivityJobs, filtered);
|
||||
console.log(`[jobFilterEngine] 步骤4-公司活跃度过滤: 剔除标题=${removedTitles.join(' | ') || '无'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 去重(同一公司、同一职位名称)
|
||||
if (config.deduplicate) {
|
||||
const beforeDedup = filtered.length;
|
||||
const beforeDedupJobs = [...filtered];
|
||||
filtered = this.deduplicateJobs(filtered);
|
||||
const dedupRemoved = beforeDedup - filtered.length;
|
||||
const dedupRemoved = beforeDedupJobs.length - filtered.length;
|
||||
if (dedupRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤5-去重: 输入${beforeDedup} 输出${filtered.length} 剔除${dedupRemoved}`);
|
||||
const removedTitles = this.getRemovedTitles(beforeDedupJobs, filtered);
|
||||
console.log(`[jobFilterEngine] 步骤5-去重: 剔除标题=${removedTitles.join(' | ') || '无'}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[jobFilterEngine] filterJobs 结束: 原始${jobs.length} 通过${filtered.length} 总剔除${jobs.length - filtered.length}`);
|
||||
const keptTitles = filtered.map((j) => j.jobTitle || '').filter(Boolean).slice(0, 5);
|
||||
console.log(`[jobFilterEngine] filterJobs 结束: 通过标题=${keptTitles.join(' | ') || '无'}`);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@@ -143,7 +162,7 @@ class JobFilterEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* 职位标题须包含配置中的每个子串(AND 关系),不扫描描述/公司名/commonSkills
|
||||
* 职位标题须包含配置中的关键词(命中任意一个即通过),不扫描描述/公司名/commonSkills
|
||||
* @param {Array} jobs
|
||||
* @param {object} config
|
||||
* @returns {Array}
|
||||
@@ -154,11 +173,11 @@ class JobFilterEngine {
|
||||
return jobs;
|
||||
}
|
||||
return jobs.filter((job) => {
|
||||
const title = `${job.jobTitle || job.jobName || job.name || ''}`.toLowerCase();
|
||||
return kws.every((kw) => {
|
||||
const title = `${job.jobTitle || ''}`.toLowerCase();
|
||||
return kws.some((kw) => {
|
||||
const k = String(kw || '').toLowerCase().trim();
|
||||
if (!k) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return title.includes(k);
|
||||
});
|
||||
@@ -251,7 +270,7 @@ class JobFilterEngine {
|
||||
|
||||
for (const job of jobs) {
|
||||
const company = (job.company || job.companyName || '').toLowerCase().trim();
|
||||
const jobName = (job.name || job.jobName || '').toLowerCase().trim();
|
||||
const jobName = (job.jobTitle || '').toLowerCase().trim();
|
||||
const key = `${company}||${jobName}`;
|
||||
|
||||
if (!seen.has(key)) {
|
||||
@@ -334,8 +353,8 @@ class JobFilterEngine {
|
||||
}
|
||||
|
||||
const jobText = [
|
||||
job.name || job.jobName || '',
|
||||
job.description || job.jobDescription || '',
|
||||
job.jobTitle || '',
|
||||
job.jobDescription || '',
|
||||
job.skills || ''
|
||||
].join(' ');
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ module.exports = (db) => {
|
||||
},
|
||||
// 投递状态
|
||||
applyStatus: {
|
||||
comment: '投递状态: pending-待投递, applied-已投递, rejected-被拒绝, accepted-已接受',
|
||||
comment: '投递状态: pending-待投递, filtered-已过滤(不符合规则未投递), applied-已投递, rejected-被拒绝, accepted-已接受, success/failed-见业务',
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: true,
|
||||
defaultValue: 'pending'
|
||||
|
||||
@@ -23,9 +23,60 @@ function isRecommendTab(text) {
|
||||
function deriveTitleIncludeKeywordsFromTabName(text) {
|
||||
const s = String(text || '').trim();
|
||||
if (!s || isRecommendTab(s)) return [];
|
||||
const m = s.match(/^([\u4e00-\u9fff]{2})[\u4e00-\u9fff]{2,}/);
|
||||
if (m) return [m[1]];
|
||||
return [];
|
||||
return normalizeTitleIncludeKeywords([], s);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将标题文本细分为更短子串,提升匹配通过率。
|
||||
* 例:项目经理/主管(上海) -> ["项目","经理","主管"]
|
||||
* @param {string} text
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function splitTitleTextToKeywords(text) {
|
||||
const raw = String(text || '')
|
||||
.replace(/[((].*?[))]/g, ' ')
|
||||
.replace(/[\/|、,,·•\-\s]+/g, ' ')
|
||||
.trim();
|
||||
if (!raw) return [];
|
||||
|
||||
const fragments = raw.split(/\s+/).filter(Boolean);
|
||||
const suffixes = [
|
||||
'工程师', '经理', '主管', '专员', '顾问', '总监', '助理',
|
||||
'开发', '测试', '运维', '产品', '前端', '后端', '算法', '架构'
|
||||
];
|
||||
const stopWords = new Set(['岗位', '职位', '方向']);
|
||||
const tokens = [];
|
||||
|
||||
for (const f of fragments) {
|
||||
if (f.length <= 1 || stopWords.has(f)) continue;
|
||||
let matched = false;
|
||||
for (const sf of suffixes) {
|
||||
if (f.endsWith(sf) && f.length > sf.length) {
|
||||
const left = f.slice(0, f.length - sf.length).trim();
|
||||
if (left && left.length >= 2 && !stopWords.has(left)) tokens.push(left);
|
||||
if (!stopWords.has(sf)) tokens.push(sf);
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) tokens.push(f);
|
||||
}
|
||||
|
||||
return [...new Set(tokens)].slice(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一清洗 titleIncludeKeywords,优先用 AI 结果,缺失时从 tabName 推导并细分。
|
||||
* @param {Array<string>} aiKeywords
|
||||
* @param {string} tabName
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function normalizeTitleIncludeKeywords(aiKeywords, tabName) {
|
||||
const aiList = Array.isArray(aiKeywords) ? aiKeywords : [];
|
||||
const fromAi = aiList.map((s) => String(s || '').trim()).filter(Boolean);
|
||||
const fromTab = splitTitleTextToKeywords(tabName);
|
||||
const merged = [...fromAi, ...fromTab].filter(Boolean);
|
||||
return [...new Set(merged)].slice(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +155,7 @@ ${snippet}
|
||||
|
||||
excludeKeywords:不适合投递的岗位特征词(如与简历方向不符的销售、客服等,按简历推断)。
|
||||
commonSkills:与简历主线一致的技能与技术栈,仅用于简历技能匹配加分,不得替代标题关键词。
|
||||
titleIncludeKeywords:职位名称(标题)中须同时包含的子串,用于过滤岗位;例如 Tab 为「售前工程师」时应有「售前」。勿把技能栈写进此数组。`;
|
||||
titleIncludeKeywords:职位名称(标题)关键词,尽量细分;例如「项目经理/主管(上海)」应输出 ["项目","经理","主管"]。勿把技能栈写进此数组。`;
|
||||
|
||||
let description = '';
|
||||
let excludeArr = [];
|
||||
@@ -125,9 +176,7 @@ titleIncludeKeywords:职位名称(标题)中须同时包含的子串,用
|
||||
description = String(parsed.description || '').slice(0, 4000);
|
||||
excludeArr = Array.isArray(parsed.excludeKeywords) ? parsed.excludeKeywords.map(String) : [];
|
||||
skillsArr = Array.isArray(parsed.commonSkills) ? parsed.commonSkills.map(String) : [];
|
||||
titleIncArr = Array.isArray(parsed.titleIncludeKeywords)
|
||||
? parsed.titleIncludeKeywords.map(String).map((s) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
titleIncArr = normalizeTitleIncludeKeywords(parsed.titleIncludeKeywords, tabName);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[job_type_ai_sync] 推荐标签 AI 创建失败,使用占位:', e.message);
|
||||
@@ -142,9 +191,7 @@ titleIncludeKeywords:职位名称(标题)中须同时包含的子串,用
|
||||
if (skillsArr.length === 0) {
|
||||
skillsArr = ['沟通', '协作'];
|
||||
}
|
||||
if (titleIncArr.length === 0) {
|
||||
titleIncArr = deriveTitleIncludeKeywordsFromTabName(tabName);
|
||||
}
|
||||
titleIncArr = normalizeTitleIncludeKeywords(titleIncArr, tabName);
|
||||
|
||||
const row = await job_types.create({
|
||||
name: tabName,
|
||||
@@ -322,7 +369,7 @@ ${labelsStr}
|
||||
- description:面向求职者的简短说明。
|
||||
- excludeKeywords:5~12 个字符串,用于过滤明显不合适的岗位。
|
||||
- commonSkills:8~20 个字符串,该方向常见技能或技术栈关键词,仅用于简历技能匹配加分,不得替代标题关键词。
|
||||
- titleIncludeKeywords:1~4 个短子串,投递时岗位标题须**同时包含**这些子串(如「售前工程师」对应含「售前」);勿把编程技能写进此数组。`;
|
||||
- titleIncludeKeywords:优先给细分词,例如「项目经理/主管(上海)」写成 ["项目","经理","主管"];勿把编程技能写进此数组。`;
|
||||
|
||||
const { content } = await aiService.callAPI(prompt, {
|
||||
systemPrompt: '你只输出合法 JSON 对象,键为 description、excludeKeywords、commonSkills、titleIncludeKeywords,不要输出其它文字。',
|
||||
@@ -341,12 +388,7 @@ ${labelsStr}
|
||||
const description = String(parsed.description || '').slice(0, 4000);
|
||||
const excludeArr = Array.isArray(parsed.excludeKeywords) ? parsed.excludeKeywords.map(String) : [];
|
||||
const skillsArr = Array.isArray(parsed.commonSkills) ? parsed.commonSkills.map(String) : [];
|
||||
let titleIncArr = Array.isArray(parsed.titleIncludeKeywords)
|
||||
? parsed.titleIncludeKeywords.map(String).map((s) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
if (titleIncArr.length === 0) {
|
||||
titleIncArr = deriveTitleIncludeKeywordsFromTabName(typeName);
|
||||
}
|
||||
let titleIncArr = normalizeTitleIncludeKeywords(parsed.titleIncludeKeywords, typeName);
|
||||
|
||||
await job_types.update(
|
||||
{
|
||||
@@ -371,5 +413,6 @@ module.exports = {
|
||||
ensureJobTypesForTabs,
|
||||
parseJsonFromAi,
|
||||
isRecommendTab,
|
||||
deriveTitleIncludeKeywordsFromTabName
|
||||
deriveTitleIncludeKeywordsFromTabName,
|
||||
normalizeTitleIncludeKeywords
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user