1
This commit is contained in:
168
api/services/ai_call_recorder.js
Normal file
168
api/services/ai_call_recorder.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const Framework = require('node-core-framework');
|
||||
|
||||
/**
|
||||
* AI调用记录服务
|
||||
* 负责记录所有AI API调用的详细信息
|
||||
*/
|
||||
class AiCallRecorder {
|
||||
/**
|
||||
* 记录AI调用
|
||||
* @param {Object} params - 调用参数
|
||||
* @param {Number} params.user_id - 用户ID
|
||||
* @param {String} params.sn_code - 设备SN码
|
||||
* @param {String} params.service_type - 服务类型(chat/completion/embedding)
|
||||
* @param {String} params.model_name - 模型名称
|
||||
* @param {Number} params.prompt_tokens - 输入Token数
|
||||
* @param {Number} params.completion_tokens - 输出Token数
|
||||
* @param {Number} params.total_tokens - 总Token数
|
||||
* @param {String} params.request_content - 请求内容
|
||||
* @param {String} params.response_content - 响应内容
|
||||
* @param {Number} params.cost_amount - 费用(元)
|
||||
* @param {String} params.status - 状态(success/failed)
|
||||
* @param {String} params.error_message - 错误信息
|
||||
* @param {Number} params.response_time - 响应时间(毫秒)
|
||||
* @param {String} params.api_provider - API提供商
|
||||
* @param {String} params.business_type - 业务类型
|
||||
* @param {String} params.reference_id - 关联业务ID
|
||||
* @returns {Promise<Object>} 记录结果
|
||||
*/
|
||||
static async record(params) {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { ai_call_records } = models;
|
||||
|
||||
if (!ai_call_records) {
|
||||
console.warn('[AI记录] ai_call_records 模型未加载');
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const record = await ai_call_records.create({
|
||||
user_id: params.user_id || null,
|
||||
sn_code: params.sn_code || null,
|
||||
service_type: params.service_type,
|
||||
model_name: params.model_name,
|
||||
prompt_tokens: params.prompt_tokens || 0,
|
||||
completion_tokens: params.completion_tokens || 0,
|
||||
total_tokens: params.total_tokens || 0,
|
||||
request_content: params.request_content || null,
|
||||
response_content: params.response_content || null,
|
||||
cost_amount: params.cost_amount || null,
|
||||
status: params.status || 'success',
|
||||
error_message: params.error_message || null,
|
||||
response_time: params.response_time || null,
|
||||
api_provider: params.api_provider || 'qwen',
|
||||
business_type: params.business_type || null,
|
||||
reference_id: params.reference_id || null,
|
||||
create_time: now,
|
||||
last_modify_time: now,
|
||||
is_delete: 0
|
||||
});
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('[AI记录] 记录失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户Token使用统计
|
||||
* @param {Number} userId - 用户ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @param {Date} options.startDate - 开始日期
|
||||
* @param {Date} options.endDate - 结束日期
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
static async getUserTokenStats(userId, options = {}) {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { ai_call_records } = models;
|
||||
const { Op } = Framework.getSequelize();
|
||||
|
||||
if (!ai_call_records) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const where = {
|
||||
user_id: userId,
|
||||
is_delete: 0
|
||||
};
|
||||
|
||||
if (options.startDate && options.endDate) {
|
||||
where.create_time = {
|
||||
[Op.between]: [options.startDate, options.endDate]
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await ai_call_records.findOne({
|
||||
where,
|
||||
attributes: [
|
||||
[Framework.getSequelize().fn('COUNT', Framework.getSequelize().col('id')), 'total_calls'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('prompt_tokens')), 'total_prompt_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('completion_tokens')), 'total_completion_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('total_tokens')), 'total_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('cost_amount')), 'total_cost'],
|
||||
[Framework.getSequelize().fn('AVG', Framework.getSequelize().col('response_time')), 'avg_response_time']
|
||||
],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[AI记录] 获取用户统计失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备Token使用统计
|
||||
* @param {String} snCode - 设备SN码
|
||||
* @param {Object} options - 查询选项
|
||||
* @param {Date} options.startDate - 开始日期
|
||||
* @param {Date} options.endDate - 结束日期
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
static async getDeviceTokenStats(snCode, options = {}) {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { ai_call_records } = models;
|
||||
const { Op } = Framework.getSequelize();
|
||||
|
||||
if (!ai_call_records) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const where = {
|
||||
sn_code: snCode,
|
||||
is_delete: 0
|
||||
};
|
||||
|
||||
if (options.startDate && options.endDate) {
|
||||
where.create_time = {
|
||||
[Op.between]: [options.startDate, options.endDate]
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await ai_call_records.findOne({
|
||||
where,
|
||||
attributes: [
|
||||
[Framework.getSequelize().fn('COUNT', Framework.getSequelize().col('id')), 'total_calls'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('prompt_tokens')), 'total_prompt_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('completion_tokens')), 'total_completion_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('total_tokens')), 'total_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('cost_amount')), 'total_cost'],
|
||||
[Framework.getSequelize().fn('AVG', Framework.getSequelize().col('response_time')), 'avg_response_time']
|
||||
],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[AI记录] 获取设备统计失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AiCallRecorder;
|
||||
@@ -1,416 +1,393 @@
|
||||
/**
|
||||
* AI智能服务
|
||||
* 提供岗位筛选、简历分析、聊天生成等AI功能
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const config = require('../../config/config');
|
||||
const AiCallRecorder = require('./ai_call_recorder.js');
|
||||
|
||||
class AIService {
|
||||
constructor(config = {}) {
|
||||
this.apiKey = config.apiKey || process.env.AI_API_KEY || '';
|
||||
this.baseURL = config.baseURL || process.env.AI_BASE_URL || 'https://api.deepseek.com';
|
||||
this.model = config.model || 'deepseek-chat';
|
||||
this.timeout = config.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}`);
|
||||
/**
|
||||
* Qwen 2.5 大模型服务
|
||||
* 集成阿里云 DashScope API,提供智能化的岗位筛选、聊天生成、简历分析等功能
|
||||
*/
|
||||
class aiService {
|
||||
constructor() {
|
||||
this.apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY;
|
||||
// 使用 DashScope 兼容 OpenAI 格式的接口
|
||||
this.apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
|
||||
// Qwen 2.5 模型:qwen-turbo(快速)、qwen-plus(增强)、qwen-max(最强)
|
||||
this.model = config.ai?.model || 'qwen-turbo';
|
||||
this.maxRetries = 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析简历竞争力
|
||||
* @param {Object} resumeData - 简历数据
|
||||
* @returns {Promise<Object>} 分析结果
|
||||
*/
|
||||
async analyzeResume(resumeData) {
|
||||
const prompt = `请分析以下简历的竞争力,并提供详细评估:
|
||||
/**
|
||||
* 调用 Qwen 2.5 API
|
||||
* @param {string} prompt - 提示词
|
||||
* @param {object} options - 配置选项
|
||||
* @returns {Promise<object>} API响应结果
|
||||
*/
|
||||
async callAPI(prompt, options = {}) {
|
||||
const startTime = Date.now();
|
||||
|
||||
const requestData = {
|
||||
model: this.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: options.systemPrompt || '你是一个专业的招聘顾问,擅长分析岗位信息、生成聊天内容和分析简历匹配度。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
temperature: options.temperature || 0.7,
|
||||
max_tokens: options.maxTokens || 2000,
|
||||
top_p: options.topP || 0.9
|
||||
};
|
||||
|
||||
const requestContent = JSON.stringify(requestData.messages);
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await axios.post(this.apiUrl, requestData, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`, // DashScope 使用 Bearer token 格式
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
const responseContent = response.data.choices?.[0]?.message?.content || '';
|
||||
const usage = response.data.usage || {};
|
||||
|
||||
// 记录AI调用(异步,不阻塞主流程)
|
||||
this.recordAiCall({
|
||||
user_id: options.user_id,
|
||||
sn_code: options.sn_code,
|
||||
service_type: options.service_type || 'completion',
|
||||
model_name: this.model,
|
||||
prompt_tokens: usage.prompt_tokens || 0,
|
||||
completion_tokens: usage.completion_tokens || 0,
|
||||
total_tokens: usage.total_tokens || 0,
|
||||
request_content: requestContent,
|
||||
response_content: responseContent,
|
||||
cost_amount: this.calculateCost(usage.total_tokens || 0),
|
||||
status: 'success',
|
||||
response_time: responseTime,
|
||||
api_provider: 'qwen',
|
||||
business_type: options.business_type,
|
||||
reference_id: options.reference_id
|
||||
}).catch(err => {
|
||||
console.warn('记录AI调用失败(不影响主流程):', err.message);
|
||||
});
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
content: responseContent
|
||||
};
|
||||
} catch (error) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
console.log(`Qwen 2.5 API调用失败 (尝试 ${attempt}/${this.maxRetries}): ${error.message}`);
|
||||
|
||||
// 记录失败的调用
|
||||
if (attempt === this.maxRetries) {
|
||||
this.recordAiCall({
|
||||
user_id: options.user_id,
|
||||
sn_code: options.sn_code,
|
||||
service_type: options.service_type || 'completion',
|
||||
model_name: this.model,
|
||||
request_content: requestContent,
|
||||
status: 'failed',
|
||||
error_message: error.message,
|
||||
response_time: responseTime,
|
||||
api_provider: 'qwen',
|
||||
business_type: options.business_type,
|
||||
reference_id: options.reference_id
|
||||
}).catch(err => {
|
||||
console.warn('记录失败调用失败:', err.message);
|
||||
});
|
||||
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
// 等待后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录AI调用
|
||||
* @param {Object} params - 调用参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async recordAiCall(params) {
|
||||
try {
|
||||
await AiCallRecorder.record(params);
|
||||
} catch (error) {
|
||||
// 记录失败不应影响主流程
|
||||
console.warn('AI调用记录失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算调用费用
|
||||
* @param {Number} totalTokens - 总Token数
|
||||
* @returns {Number} 费用(元)
|
||||
*/
|
||||
calculateCost(totalTokens) {
|
||||
// 阿里云 Qwen 价格(元/1000 tokens)
|
||||
// qwen-turbo: ¥0.003, qwen-plus: ¥0.004, qwen-max: ¥0.12
|
||||
// 这里使用 qwen-turbo 的价格作为默认值
|
||||
const pricePerThousand = 0.003;
|
||||
return (totalTokens / 1000) * pricePerThousand;
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位智能筛选
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @returns {Promise<object>} 筛选结果
|
||||
*/
|
||||
async analyzeJob(jobInfo, resumeInfo) {
|
||||
const prompt = `
|
||||
请分析以下岗位信息,并给出详细的评估结果:
|
||||
|
||||
岗位信息:
|
||||
- 公司名称:${jobInfo.companyName || '未知'}
|
||||
- 职位名称:${jobInfo.jobTitle || '未知'}
|
||||
- 薪资范围:${jobInfo.salary || '未知'}
|
||||
- 工作地点:${jobInfo.location || '未知'}
|
||||
- 岗位描述:${jobInfo.description || '未知'}
|
||||
- 技能要求:${jobInfo.skills || '未知'}
|
||||
|
||||
简历信息:
|
||||
- 姓名: ${resumeData.fullName || '未知'}
|
||||
- 工作年限: ${resumeData.workYears || '未知'}
|
||||
- 教育背景: ${resumeData.education || '未知'}
|
||||
- 期望职位: ${resumeData.expectedPosition || '未知'}
|
||||
- 期望薪资: ${resumeData.expectedSalary || '未知'}
|
||||
- 技能标签: ${Array.isArray(resumeData.skills) ? resumeData.skills.join('、') : '未知'}
|
||||
- 工作经历: ${resumeData.workExperience || '未提供'}
|
||||
- 项目经历: ${resumeData.projectExperience || '未提供'}
|
||||
- 技能标签:${resumeInfo.skills || '未知'}
|
||||
- 工作经验:${resumeInfo.experience || '未知'}
|
||||
- 教育背景:${resumeInfo.education || '未知'}
|
||||
- 期望薪资:${resumeInfo.expectedSalary || '未知'}
|
||||
|
||||
请从以下维度进行评估(1-100分):
|
||||
1. 技术能力
|
||||
2. 项目经验
|
||||
3. 教育背景
|
||||
4. 工作年限匹配度
|
||||
5. 综合竞争力
|
||||
请从以下维度进行分析:
|
||||
1. 技能匹配度(0-100分)
|
||||
2. 经验匹配度(0-100分)
|
||||
3. 薪资合理性(0-100分)
|
||||
4. 公司质量评估(0-100分)
|
||||
5. 是否为外包岗位(是/否)
|
||||
6. 综合推荐指数(0-100分)
|
||||
7. 详细分析说明
|
||||
8. 投递建议
|
||||
|
||||
返回JSON格式:
|
||||
请以JSON格式返回结果。
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的招聘分析师,擅长评估岗位与简历的匹配度。请提供客观、专业的分析结果。',
|
||||
temperature: 0.3,
|
||||
business_type: 'job_analysis',
|
||||
service_type: 'completion'
|
||||
});
|
||||
|
||||
try {
|
||||
// 尝试解析JSON响应
|
||||
const analysis = JSON.parse(result.content);
|
||||
return {
|
||||
analysis: analysis
|
||||
};
|
||||
} catch (parseError) {
|
||||
// 如果解析失败,返回原始内容
|
||||
return {
|
||||
analysis: {
|
||||
content: result.content,
|
||||
parseError: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成个性化聊天内容
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @param {string} chatType - 聊天类型 (greeting/interview/followup)
|
||||
* @returns {Promise<object>} 聊天内容
|
||||
*/
|
||||
async generateChatContent(jobInfo, resumeInfo, chatType = 'greeting') {
|
||||
const chatTypeMap = {
|
||||
'greeting': '初次打招呼',
|
||||
'interview': '面试邀约',
|
||||
'followup': '跟进沟通'
|
||||
};
|
||||
|
||||
const prompt = `
|
||||
请为以下场景生成个性化的聊天内容:
|
||||
|
||||
聊天类型:${chatTypeMap[chatType] || chatType}
|
||||
|
||||
岗位信息:
|
||||
- 公司名称:${jobInfo.companyName || '未知'}
|
||||
- 职位名称:${jobInfo.jobTitle || '未知'}
|
||||
- 技能要求:${jobInfo.skills || '未知'}
|
||||
|
||||
简历信息:
|
||||
- 技能标签:${resumeInfo.skills || '未知'}
|
||||
- 工作经验:${resumeInfo.experience || '未知'}
|
||||
- 项目经验:${resumeInfo.projects || '未知'}
|
||||
|
||||
要求:
|
||||
1. 内容要自然、专业、个性化
|
||||
2. 突出简历与岗位的匹配点
|
||||
3. 避免过于机械化的表达
|
||||
4. 长度控制在100-200字
|
||||
5. 体现求职者的诚意和热情
|
||||
|
||||
请直接返回聊天内容,不需要其他格式。
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的招聘沟通专家,擅长生成自然、专业的求职聊天内容。',
|
||||
temperature: 0.8,
|
||||
business_type: 'chat_generation',
|
||||
service_type: 'chat',
|
||||
reference_id: jobInfo.jobId || jobInfo.id
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析简历要素
|
||||
* @param {string} resumeText - 简历文本内容
|
||||
* @returns {Promise<object>} 简历分析结果
|
||||
*/
|
||||
async analyzeResume(resumeText) {
|
||||
const prompt = `
|
||||
请分析以下简历内容,并返回 JSON 格式的分析结果:
|
||||
|
||||
简历内容:
|
||||
${resumeText}
|
||||
|
||||
请按以下格式返回 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: []
|
||||
};
|
||||
}
|
||||
}
|
||||
"skillTags": ["技能1", "技能2", "技能3"], // 技能标签数组(编程语言、框架、工具等)
|
||||
"strengths": "核心优势描述", // 简历的优势和亮点
|
||||
"weaknesses": "不足之处描述", // 简历的不足或需要改进的地方
|
||||
"careerSuggestion": "职业发展建议", // 针对该简历的职业发展方向和建议
|
||||
"competitiveness": 75 // 竞争力评分(0-100的整数),综合考虑工作年限、技能、经验等因素
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
let instance = null;
|
||||
要求:
|
||||
1. skillTags 必须是字符串数组
|
||||
2. strengths、weaknesses、careerSuggestion 是字符串描述
|
||||
3. competitiveness 必须是 0-100 之间的整数
|
||||
4. 所有字段都必须返回,如果没有相关信息,使用空数组或空字符串
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* 获取AI服务实例
|
||||
* @param {Object} config - 配置选项
|
||||
* @returns {AIService}
|
||||
*/
|
||||
getInstance(config) {
|
||||
if (!instance) {
|
||||
instance = new AIService(config);
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的简历分析师,擅长分析简历的核心要素、优势劣势、竞争力评分和职业发展建议。请以 JSON 格式返回分析结果,确保格式正确。',
|
||||
temperature: 0.3,
|
||||
maxTokens: 1500,
|
||||
business_type: 'resume_analysis',
|
||||
service_type: 'completion'
|
||||
});
|
||||
|
||||
try {
|
||||
// 尝试从返回内容中提取 JSON
|
||||
let content = result.content.trim();
|
||||
|
||||
// 如果返回内容被代码块包裹,提取其中的 JSON
|
||||
const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || content.match(/(\{[\s\S]*\})/);
|
||||
if (jsonMatch) {
|
||||
content = jsonMatch[1];
|
||||
}
|
||||
|
||||
const analysis = JSON.parse(content);
|
||||
return {
|
||||
analysis: analysis
|
||||
};
|
||||
} catch (parseError) {
|
||||
console.error(`[AI服务] 简历分析结果解析失败:`, parseError);
|
||||
console.error(`[AI服务] 原始内容:`, result.content);
|
||||
return {
|
||||
analysis: {
|
||||
content: result.content,
|
||||
parseError: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建新的AI服务实例
|
||||
* @param {Object} config - 配置选项
|
||||
* @returns {AIService}
|
||||
*/
|
||||
createInstance(config) {
|
||||
return new AIService(config);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 生成面试邀约内容
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} chatHistory - 聊天历史
|
||||
* @returns {Promise<object>} 面试邀约内容
|
||||
*/
|
||||
async generateInterviewInvitation(jobInfo, chatHistory) {
|
||||
const prompt = `
|
||||
请基于以下信息生成面试邀约内容:
|
||||
|
||||
岗位信息:
|
||||
- 公司名称:${jobInfo.companyName || '未知'}
|
||||
- 职位名称:${jobInfo.jobTitle || '未知'}
|
||||
- 工作地点:${jobInfo.location || '未知'}
|
||||
|
||||
聊天历史:
|
||||
${chatHistory || '无'}
|
||||
|
||||
要求:
|
||||
1. 表达面试邀约的诚意
|
||||
2. 提供灵活的时间选择
|
||||
3. 说明面试形式和地点
|
||||
4. 体现对候选人的重视
|
||||
5. 语言自然、专业
|
||||
|
||||
请直接返回面试邀约内容。
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的HR,擅长生成面试邀约内容。',
|
||||
temperature: 0.6,
|
||||
business_type: 'interview_invitation',
|
||||
service_type: 'chat'
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别外包岗位
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @returns {Promise<object>} 外包识别结果
|
||||
*/
|
||||
async identifyOutsourcingJob(jobInfo) {
|
||||
const prompt = `
|
||||
请分析以下岗位信息,判断是否为外包岗位:
|
||||
|
||||
岗位信息:
|
||||
- 公司名称:${jobInfo.companyName || '未知'}
|
||||
- 职位名称:${jobInfo.jobTitle || '未知'}
|
||||
- 岗位描述:${jobInfo.description || '未知'}
|
||||
- 技能要求:${jobInfo.skills || '未知'}
|
||||
- 工作地点:${jobInfo.location || '未知'}
|
||||
|
||||
外包岗位特征:
|
||||
1. 公司名称包含"外包"、"派遣"、"人力"等关键词
|
||||
2. 岗位描述提到"项目外包"、"驻场开发"等
|
||||
3. 技能要求过于宽泛或具体
|
||||
4. 工作地点频繁变动
|
||||
5. 薪资结构不明确
|
||||
|
||||
请判断是否为外包岗位,并给出详细分析。
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的岗位分析师,擅长识别外包岗位的特征。',
|
||||
temperature: 0.3,
|
||||
business_type: 'outsourcing_detection',
|
||||
service_type: 'completion'
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new aiService();
|
||||
|
||||
Reference in New Issue
Block a user