416 lines
12 KiB
JavaScript
416 lines
12 KiB
JavaScript
/**
|
||
* AI智能服务
|
||
* 提供岗位筛选、简历分析、聊天生成等AI功能
|
||
*/
|
||
|
||
const axios = require('axios');
|
||
const config = require('../../config/config');
|
||
|
||
class AIService {
|
||
constructor() {
|
||
this.apiKey = config.ai.apiKey;
|
||
this.baseURL = config.ai.baseUrl;
|
||
this.model = config.ai.model;
|
||
this.timeout = 30000;
|
||
|
||
// 创建axios实例
|
||
this.client = axios.create({
|
||
baseURL: this.baseURL,
|
||
timeout: this.timeout,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `${this.apiKey}`
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 调用AI接口
|
||
* @param {Array} messages - 消息数组
|
||
* @param {Object} options - 额外选项
|
||
* @returns {Promise<String>} AI响应内容
|
||
*/
|
||
async chat(messages, options = {}) {
|
||
try {
|
||
const response = await this.client.post('/v1/chat/completions', {
|
||
model: this.model,
|
||
messages,
|
||
temperature: options.temperature || 0.7,
|
||
max_tokens: options.max_tokens || 2000,
|
||
...options
|
||
});
|
||
|
||
return response.data.choices[0].message.content;
|
||
} catch (error) {
|
||
console.warn('AI服务调用失败:', error.message);
|
||
throw new Error(`AI服务调用失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 分析简历竞争力
|
||
* @param {Object} resumeData - 简历数据
|
||
* @returns {Promise<Object>} 分析结果
|
||
*/
|
||
async analyzeResume(resumeData) {
|
||
const prompt = `请分析以下简历的竞争力,并提供详细评估:
|
||
|
||
简历信息:
|
||
- 姓名: ${resumeData.fullName || '未知'}
|
||
- 工作年限: ${resumeData.workYears || '未知'}
|
||
- 教育背景: ${resumeData.education || '未知'}
|
||
- 期望职位: ${resumeData.expectedPosition || '未知'}
|
||
- 期望薪资: ${resumeData.expectedSalary || '未知'}
|
||
- 技能标签: ${Array.isArray(resumeData.skills) ? resumeData.skills.join('、') : '未知'}
|
||
- 工作经历: ${resumeData.workExperience || '未提供'}
|
||
- 项目经历: ${resumeData.projectExperience || '未提供'}
|
||
|
||
请从以下维度进行评估(1-100分):
|
||
1. 技术能力
|
||
2. 项目经验
|
||
3. 教育背景
|
||
4. 工作年限匹配度
|
||
5. 综合竞争力
|
||
|
||
返回JSON格式:
|
||
{
|
||
"overallScore": 总分(1-100),
|
||
"technicalScore": 技术能力分(1-100),
|
||
"projectScore": 项目经验分(1-100),
|
||
"educationScore": 教育背景分(1-100),
|
||
"experienceScore": 工作年限分(1-100),
|
||
"strengths": ["优势1", "优势2", "优势3"],
|
||
"weaknesses": ["不足1", "不足2"],
|
||
"suggestions": ["建议1", "建议2", "建议3"],
|
||
"keySkills": ["核心技能1", "核心技能2"],
|
||
"marketCompetitiveness": "市场竞争力描述"
|
||
}`;
|
||
|
||
const messages = [
|
||
{ role: 'system', content: '你是一个专业的HR和招聘顾问,擅长分析简历和评估候选人竞争力。' },
|
||
{ role: 'user', content: prompt }
|
||
];
|
||
|
||
try {
|
||
const response = await this.chat(messages, { temperature: 0.3 });
|
||
// 提取JSON部分
|
||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||
if (jsonMatch) {
|
||
return JSON.parse(jsonMatch[0]);
|
||
}
|
||
throw new Error('AI返回格式不正确');
|
||
} catch (error) {
|
||
console.warn('简历分析失败:', error);
|
||
// 返回默认值
|
||
return {
|
||
overallScore: 60,
|
||
technicalScore: 60,
|
||
projectScore: 60,
|
||
educationScore: 60,
|
||
experienceScore: 60,
|
||
strengths: ['待AI分析'],
|
||
weaknesses: ['待AI分析'],
|
||
suggestions: ['请稍后重试'],
|
||
keySkills: Array.isArray(resumeData.skills) ? resumeData.skills.slice(0, 5) : [],
|
||
marketCompetitiveness: '待AI分析'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 岗位匹配度评估
|
||
* @param {Object} jobData - 岗位数据
|
||
* @param {Object} resumeData - 简历数据
|
||
* @returns {Promise<Object>} 匹配结果
|
||
*/
|
||
async matchJobWithResume(jobData, resumeData) {
|
||
const prompt = `请评估以下岗位与简历的匹配度:
|
||
|
||
【岗位信息】
|
||
- 职位名称: ${jobData.jobTitle || '未知'}
|
||
- 公司名称: ${jobData.companyName || '未知'}
|
||
- 薪资范围: ${jobData.salary || '未知'}
|
||
- 工作地点: ${jobData.location || '未知'}
|
||
- 工作经验要求: ${jobData.experienceRequired || '未知'}
|
||
- 学历要求: ${jobData.educationRequired || '未知'}
|
||
- 岗位描述: ${jobData.jobDescription || '未提供'}
|
||
- 技能要求: ${jobData.skillsRequired || '未提供'}
|
||
|
||
【简历信息】
|
||
- 工作年限: ${resumeData.workYears || '未知'}
|
||
- 教育背景: ${resumeData.education || '未知'}
|
||
- 技能标签: ${Array.isArray(resumeData.skills) ? resumeData.skills.join('、') : '未知'}
|
||
- 期望职位: ${resumeData.expectedPosition || '未知'}
|
||
- 期望薪资: ${resumeData.expectedSalary || '未知'}
|
||
|
||
请分析:
|
||
1. 技能匹配度
|
||
2. 经验匹配度
|
||
3. 薪资匹配度
|
||
4. 是否为外包岗位(根据公司名称、岗位描述判断)
|
||
5. 综合推荐度
|
||
|
||
返回JSON格式:
|
||
{
|
||
"matchScore": 匹配度分数(1-100),
|
||
"skillMatch": 技能匹配度(1-100),
|
||
"experienceMatch": 经验匹配度(1-100),
|
||
"salaryMatch": 薪资匹配度(1-100),
|
||
"isOutsourcing": 是否外包(true/false),
|
||
"outsourcingConfidence": 外包判断置信度(0-1),
|
||
"recommendLevel": "推荐等级(excellent/good/medium/low)",
|
||
"matchReasons": ["匹配原因1", "匹配原因2"],
|
||
"concerns": ["顾虑点1", "顾虑点2"],
|
||
"applyAdvice": "投递建议"
|
||
}`;
|
||
|
||
const messages = [
|
||
{ role: 'system', content: '你是一个专业的职业规划师,擅长评估岗位与求职者的匹配度。' },
|
||
{ role: 'user', content: prompt }
|
||
];
|
||
|
||
try {
|
||
const response = await this.chat(messages, { temperature: 0.3 });
|
||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||
if (jsonMatch) {
|
||
return JSON.parse(jsonMatch[0]);
|
||
}
|
||
throw new Error('AI返回格式不正确');
|
||
} catch (error) {
|
||
console.warn('岗位匹配分析失败:', error);
|
||
// 返回默认值
|
||
return {
|
||
matchScore: 50,
|
||
skillMatch: 50,
|
||
experienceMatch: 50,
|
||
salaryMatch: 50,
|
||
isOutsourcing: false,
|
||
outsourcingConfidence: 0,
|
||
recommendLevel: 'medium',
|
||
matchReasons: ['待AI分析'],
|
||
concerns: ['待AI分析'],
|
||
applyAdvice: '建议人工审核'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量评估岗位(用于智能筛选)
|
||
* @param {Array} jobs - 岗位列表
|
||
* @param {Object} resumeData - 简历数据
|
||
* @returns {Promise<Array>} 评估结果列表
|
||
*/
|
||
async batchMatchJobs(jobs, resumeData) {
|
||
const results = [];
|
||
|
||
// 限制并发数量,避免API限流
|
||
const concurrency = 3;
|
||
for (let i = 0; i < jobs.length; i += concurrency) {
|
||
const batch = jobs.slice(i, i + concurrency);
|
||
const batchPromises = batch.map(job =>
|
||
this.matchJobWithResume(job, resumeData).catch(err => {
|
||
console.warn(`岗位${job.jobId}匹配失败:`, err.message);
|
||
return {
|
||
jobId: job.jobId,
|
||
matchScore: 0,
|
||
error: err.message
|
||
};
|
||
})
|
||
);
|
||
|
||
const batchResults = await Promise.all(batchPromises);
|
||
results.push(...batchResults);
|
||
|
||
// 避免请求过快,休眠一下
|
||
if (i + concurrency < jobs.length) {
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 生成聊天内容
|
||
* @param {Object} context - 聊天上下文
|
||
* @returns {Promise<String>} 生成的聊天内容
|
||
*/
|
||
async generateChatContent(context) {
|
||
const { jobInfo, resumeInfo, chatType = 'greeting', previousMessages = [] } = context;
|
||
|
||
let prompt = '';
|
||
|
||
switch (chatType) {
|
||
case 'greeting':
|
||
prompt = `作为求职者,向HR发送第一条消息表达对以下岗位的兴趣:
|
||
岗位: ${jobInfo.jobTitle}
|
||
公司: ${jobInfo.companyName}
|
||
要求: 简洁、专业、突出自己的优势,不超过100字`;
|
||
break;
|
||
|
||
case 'follow_up':
|
||
prompt = `HR已查看简历但未回复,需要发送一条礼貌的跟进消息:
|
||
岗位: ${jobInfo.jobTitle}
|
||
要求: 礼貌、不唐突、展现持续兴趣,不超过80字`;
|
||
break;
|
||
|
||
case 'interview_confirm':
|
||
prompt = `HR发出面试邀约,需要确认并表达感谢:
|
||
岗位: ${jobInfo.jobTitle}
|
||
面试时间: ${context.interviewTime || '待定'}
|
||
要求: 专业、感谢、确认参加,不超过60字`;
|
||
break;
|
||
|
||
case 'reply':
|
||
prompt = `HR说: "${context.hrMessage}"
|
||
请作为求职者回复,要求: 自然、专业、回答问题,不超过100字`;
|
||
break;
|
||
|
||
default:
|
||
prompt = `生成一条针对${jobInfo.jobTitle}岗位的沟通消息`;
|
||
}
|
||
|
||
const messages = [
|
||
{ role: 'system', content: '你是一个求职者,需要与HR进行专业、礼貌的沟通。回复要简洁、真诚、突出优势。' },
|
||
...previousMessages.map(msg => ({
|
||
role: msg.role,
|
||
content: msg.content
|
||
})),
|
||
{ role: 'user', content: prompt }
|
||
];
|
||
|
||
try {
|
||
const response = await this.chat(messages, { temperature: 0.8, max_tokens: 200 });
|
||
return response.trim();
|
||
} catch (error) {
|
||
console.warn('生成聊天内容失败:', error);
|
||
// 返回默认模板
|
||
switch (chatType) {
|
||
case 'greeting':
|
||
return `您好,我对贵司的${jobInfo.jobTitle}岗位非常感兴趣,我有相关工作经验,期待能有机会详细沟通。`;
|
||
case 'follow_up':
|
||
return `您好,不知道您对我的简历是否感兴趣?期待您的回复。`;
|
||
case 'interview_confirm':
|
||
return `好的,感谢您的面试邀约,我会准时参加。`;
|
||
default:
|
||
return `您好,期待与您沟通。`;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 判断是否为面试邀约
|
||
* @param {String} message - HR消息内容
|
||
* @returns {Promise<Object>} 判断结果
|
||
*/
|
||
async detectInterviewInvitation(message) {
|
||
const prompt = `判断以下HR消息是否为面试邀约,并提取关键信息:
|
||
|
||
消息内容: "${message}"
|
||
|
||
返回JSON格式:
|
||
{
|
||
"isInterview": 是否为面试邀约(true/false),
|
||
"confidence": 置信度(0-1),
|
||
"interviewType": "面试类型(phone/video/onsite/unknown)",
|
||
"interviewTime": "面试时间(如果提到)",
|
||
"interviewLocation": "面试地点(如果提到)",
|
||
"needReply": 是否需要回复确认(true/false)
|
||
}`;
|
||
|
||
const messages = [
|
||
{ role: 'system', content: '你是一个文本分析专家,擅长识别面试邀约信息。' },
|
||
{ role: 'user', content: prompt }
|
||
];
|
||
|
||
try {
|
||
const response = await this.chat(messages, { temperature: 0.1 });
|
||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||
if (jsonMatch) {
|
||
return JSON.parse(jsonMatch[0]);
|
||
}
|
||
throw new Error('AI返回格式不正确');
|
||
} catch (error) {
|
||
console.warn('面试邀约判断失败:', error);
|
||
// 简单的关键词判断作为降级方案
|
||
const keywords = ['面试', '邀请', '来面', '约您', '见面', '电话沟通', '视频面试'];
|
||
const isInterview = keywords.some(kw => message.includes(kw));
|
||
|
||
return {
|
||
isInterview,
|
||
confidence: isInterview ? 0.7 : 0.3,
|
||
interviewType: 'unknown',
|
||
interviewTime: null,
|
||
interviewLocation: null,
|
||
needReply: isInterview
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 分析HR反馈情感
|
||
* @param {String} message - HR消息内容
|
||
* @returns {Promise<Object>} 情感分析结果
|
||
*/
|
||
async analyzeSentiment(message) {
|
||
const prompt = `分析以下HR消息的情感倾向:
|
||
|
||
消息: "${message}"
|
||
|
||
返回JSON格式:
|
||
{
|
||
"sentiment": "情感倾向(positive/neutral/negative)",
|
||
"interest": "兴趣程度(high/medium/low)",
|
||
"urgency": "紧急程度(high/medium/low)",
|
||
"keywords": ["关键词1", "关键词2"]
|
||
}`;
|
||
|
||
const messages = [
|
||
{ role: 'system', content: '你是一个情感分析专家。' },
|
||
{ role: 'user', content: prompt }
|
||
];
|
||
|
||
try {
|
||
const response = await this.chat(messages, { temperature: 0.1 });
|
||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||
if (jsonMatch) {
|
||
return JSON.parse(jsonMatch[0]);
|
||
}
|
||
throw new Error('AI返回格式不正确');
|
||
} catch (error) {
|
||
console.warn('情感分析失败:', error);
|
||
return {
|
||
sentiment: 'neutral',
|
||
interest: 'medium',
|
||
urgency: 'low',
|
||
keywords: []
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出单例
|
||
let instance = null;
|
||
|
||
module.exports = {
|
||
/**
|
||
* 获取AI服务实例
|
||
* @returns {AIService}
|
||
*/
|
||
getInstance() {
|
||
if (!instance) {
|
||
instance = new AIService();
|
||
}
|
||
return instance;
|
||
},
|
||
|
||
/**
|
||
* 创建新的AI服务实例
|
||
* @returns {AIService}
|
||
*/
|
||
createInstance() {
|
||
return new AIService();
|
||
}
|
||
};
|
||
|