1
This commit is contained in:
@@ -180,34 +180,50 @@ class aiService {
|
||||
*/
|
||||
async analyzeResume(resumeText) {
|
||||
const prompt = `
|
||||
请分析以下简历内容,提取核心要素:
|
||||
请分析以下简历内容,并返回 JSON 格式的分析结果:
|
||||
|
||||
简历内容:
|
||||
${resumeText}
|
||||
|
||||
请提取以下信息:
|
||||
1. 技能标签(编程语言、框架、工具等)
|
||||
2. 工作经验(年限、行业、项目等)
|
||||
3. 教育背景(学历、专业、证书等)
|
||||
4. 期望薪资范围
|
||||
5. 期望工作地点
|
||||
6. 核心优势
|
||||
7. 职业发展方向
|
||||
请按以下格式返回 JSON 结果:
|
||||
{
|
||||
"skillTags": ["技能1", "技能2", "技能3"], // 技能标签数组(编程语言、框架、工具等)
|
||||
"strengths": "核心优势描述", // 简历的优势和亮点
|
||||
"weaknesses": "不足之处描述", // 简历的不足或需要改进的地方
|
||||
"careerSuggestion": "职业发展建议", // 针对该简历的职业发展方向和建议
|
||||
"competitiveness": 75 // 竞争力评分(0-100的整数),综合考虑工作年限、技能、经验等因素
|
||||
}
|
||||
|
||||
请以JSON格式返回结果。
|
||||
要求:
|
||||
1. skillTags 必须是字符串数组
|
||||
2. strengths、weaknesses、careerSuggestion 是字符串描述
|
||||
3. competitiveness 必须是 0-100 之间的整数
|
||||
4. 所有字段都必须返回,如果没有相关信息,使用空数组或空字符串
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的简历分析师,擅长提取简历的核心要素和关键信息。',
|
||||
temperature: 0.2
|
||||
systemPrompt: '你是一个专业的简历分析师,擅长分析简历的核心要素、优势劣势、竞争力评分和职业发展建议。请以 JSON 格式返回分析结果,确保格式正确。',
|
||||
temperature: 0.3,
|
||||
maxTokens: 1500
|
||||
});
|
||||
|
||||
try {
|
||||
const analysis = JSON.parse(result.content);
|
||||
// 尝试从返回内容中提取 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,
|
||||
|
||||
@@ -181,12 +181,103 @@ class ResumeManager {
|
||||
console.log(`[简历管理] 简历已创建 - ID: ${resumeId}`);
|
||||
}
|
||||
|
||||
// 二期规划:AI 分析暂时禁用,使用简单的文本匹配
|
||||
console.log(`[简历管理] AI分析已禁用(二期规划),使用文本匹配过滤`);
|
||||
// 调用 AI 分析简历并更新 AI 字段
|
||||
|
||||
try {
|
||||
await this.analyze_resume_with_ai(resumeId, resumeInfo);
|
||||
console.log(`[简历管理] AI 分析完成并已更新到数据库`);
|
||||
} catch (error) {
|
||||
console.error(`[简历管理] AI 分析失败:`, error);
|
||||
// AI 分析失败不影响主流程,继续返回成功
|
||||
}
|
||||
|
||||
return { resumeId, message: existingResume ? '简历更新成功' : '简历创建成功' };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建用于 AI 分析的简历文本
|
||||
* @param {object} resumeInfo - 简历信息对象
|
||||
* @returns {string} 简历文本内容
|
||||
*/
|
||||
build_resume_text_for_ai(resumeInfo) {
|
||||
const parts = [];
|
||||
|
||||
// 基本信息
|
||||
if (resumeInfo.fullName) parts.push(`姓名:${resumeInfo.fullName}`);
|
||||
if (resumeInfo.gender) parts.push(`性别:${resumeInfo.gender}`);
|
||||
if (resumeInfo.age) parts.push(`年龄:${resumeInfo.age}`);
|
||||
if (resumeInfo.location) parts.push(`所在地:${resumeInfo.location}`);
|
||||
|
||||
// 教育背景
|
||||
if (resumeInfo.education) parts.push(`学历:${resumeInfo.education}`);
|
||||
if (resumeInfo.school) parts.push(`毕业院校:${resumeInfo.school}`);
|
||||
if (resumeInfo.major) parts.push(`专业:${resumeInfo.major}`);
|
||||
if (resumeInfo.graduationYear) parts.push(`毕业年份:${resumeInfo.graduationYear}`);
|
||||
|
||||
// 工作经验
|
||||
if (resumeInfo.workYears) parts.push(`工作年限:${resumeInfo.workYears}`);
|
||||
if (resumeInfo.currentPosition) parts.push(`当前职位:${resumeInfo.currentPosition}`);
|
||||
if (resumeInfo.currentCompany) parts.push(`当前公司:${resumeInfo.currentCompany}`);
|
||||
|
||||
// 期望信息
|
||||
if (resumeInfo.expectedPosition) parts.push(`期望职位:${resumeInfo.expectedPosition}`);
|
||||
if (resumeInfo.expectedSalary) parts.push(`期望薪资:${resumeInfo.expectedSalary}`);
|
||||
if (resumeInfo.expectedLocation) parts.push(`期望地点:${resumeInfo.expectedLocation}`);
|
||||
|
||||
// 技能描述
|
||||
if (resumeInfo.skillDescription) parts.push(`技能描述:${resumeInfo.skillDescription}`);
|
||||
|
||||
// 工作经历
|
||||
if (resumeInfo.workExperience) {
|
||||
try {
|
||||
const workExp = JSON.parse(resumeInfo.workExperience);
|
||||
if (Array.isArray(workExp) && workExp.length > 0) {
|
||||
parts.push('\n工作经历:');
|
||||
workExp.forEach(work => {
|
||||
const workText = [
|
||||
work.company && `公司:${work.company}`,
|
||||
work.position && `职位:${work.position}`,
|
||||
work.startDate && work.endDate && `时间:${work.startDate} - ${work.endDate}`,
|
||||
work.content && `工作内容:${work.content}`
|
||||
].filter(Boolean).join(',');
|
||||
if (workText) parts.push(workText);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 项目经验
|
||||
if (resumeInfo.projectExperience) {
|
||||
try {
|
||||
const projectExp = JSON.parse(resumeInfo.projectExperience);
|
||||
if (Array.isArray(projectExp) && projectExp.length > 0) {
|
||||
parts.push('\n项目经验:');
|
||||
projectExp.forEach(project => {
|
||||
const projectText = [
|
||||
project.name && `项目名称:${project.name}`,
|
||||
project.role && `角色:${project.role}`,
|
||||
project.description && `描述:${project.description}`
|
||||
].filter(Boolean).join(',');
|
||||
if (projectText) parts.push(projectText);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 简历完整内容
|
||||
if (resumeInfo.resumeContent) {
|
||||
parts.push('\n简历详细内容:');
|
||||
parts.push(resumeInfo.resumeContent);
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从描述中提取技能标签
|
||||
* @param {string} description - 描述文本
|
||||
@@ -253,16 +344,23 @@ ${resumeInfo.skillDescription}
|
||||
// 解析AI返回的结果
|
||||
const analysis = this.parse_ai_analysis(aiAnalysis, resumeInfo);
|
||||
|
||||
// 更新简历的AI分析字段
|
||||
await resume_info.update({
|
||||
aiSkillTags: JSON.stringify(analysis.skillTags),
|
||||
aiStrengths: analysis.strengths,
|
||||
aiWeaknesses: analysis.weaknesses,
|
||||
aiCareerSuggestion: analysis.careerSuggestion,
|
||||
aiCompetitiveness: analysis.competitiveness
|
||||
}, { where: { id: resumeId } });
|
||||
// 确保所有字段都有值
|
||||
const updateData = {
|
||||
aiSkillTags: JSON.stringify(analysis.skillTags || []),
|
||||
aiStrengths: analysis.strengths || '',
|
||||
aiWeaknesses: analysis.weaknesses || '',
|
||||
aiCareerSuggestion: analysis.careerSuggestion || '',
|
||||
aiCompetitiveness: parseInt(analysis.competitiveness || 70, 10)
|
||||
};
|
||||
|
||||
console.log(`[简历管理] AI分析完成 - 竞争力评分: ${analysis.competitiveness}`);
|
||||
// 确保竞争力评分在 0-100 范围内
|
||||
if (updateData.aiCompetitiveness < 0) updateData.aiCompetitiveness = 0;
|
||||
if (updateData.aiCompetitiveness > 100) updateData.aiCompetitiveness = 100;
|
||||
|
||||
// 更新简历的AI分析字段
|
||||
await resume_info.update(updateData, { where: { resumeId: resumeId } });
|
||||
|
||||
console.log(`[简历管理] AI分析完成 - 竞争力评分: ${updateData.aiCompetitiveness}, 技能标签: ${updateData.aiSkillTags}`);
|
||||
|
||||
return analysis;
|
||||
} catch (error) {
|
||||
@@ -271,9 +369,26 @@ ${resumeInfo.skillDescription}
|
||||
fullName: resumeInfo.fullName
|
||||
});
|
||||
|
||||
// 如果AI分析失败,使用基于规则的默认分析
|
||||
// 如果AI分析失败,使用基于规则的默认分析,并保存到数据库
|
||||
const defaultAnalysis = this.get_default_analysis(resumeInfo);
|
||||
|
||||
// 保存默认分析结果到数据库
|
||||
const updateData = {
|
||||
aiSkillTags: JSON.stringify(defaultAnalysis.skillTags || []),
|
||||
aiStrengths: defaultAnalysis.strengths || '',
|
||||
aiWeaknesses: defaultAnalysis.weaknesses || '',
|
||||
aiCareerSuggestion: defaultAnalysis.careerSuggestion || '',
|
||||
aiCompetitiveness: parseInt(defaultAnalysis.competitiveness || 70, 10)
|
||||
};
|
||||
|
||||
// 确保竞争力评分在 0-100 范围内
|
||||
if (updateData.aiCompetitiveness < 0) updateData.aiCompetitiveness = 0;
|
||||
if (updateData.aiCompetitiveness > 100) updateData.aiCompetitiveness = 100;
|
||||
|
||||
await resume_info.update(updateData, { where: { resumeId: resumeId } });
|
||||
|
||||
console.log(`[简历管理] 使用默认分析结果 - 竞争力评分: ${updateData.aiCompetitiveness}`);
|
||||
|
||||
return defaultAnalysis;
|
||||
}
|
||||
}
|
||||
@@ -286,39 +401,62 @@ ${resumeInfo.skillDescription}
|
||||
*/
|
||||
parse_ai_analysis(aiResponse, resumeInfo) {
|
||||
try {
|
||||
// 尝试从AI响应中解析JSON
|
||||
const content = aiResponse.content || aiResponse.analysis?.content || '';
|
||||
// aiService.analyzeResume 返回格式: { analysis: {...} } 或 { analysis: { content: "...", parseError: true } }
|
||||
const analysis = aiResponse.analysis;
|
||||
|
||||
// 如果AI返回的是JSON格式
|
||||
if (content.includes('{') && content.includes('}')) {
|
||||
// 如果解析失败,analysis 会有 parseError 标记
|
||||
if (analysis && analysis.parseError) {
|
||||
console.warn(`[简历管理] AI分析结果解析失败,使用默认分析`);
|
||||
return this.get_default_analysis(resumeInfo);
|
||||
}
|
||||
|
||||
// 如果解析成功,analysis 直接是解析后的对象
|
||||
if (analysis && typeof analysis === 'object' && !analysis.parseError) {
|
||||
return {
|
||||
skillTags: analysis.skillTags || analysis.技能标签 || [],
|
||||
strengths: analysis.strengths || analysis.优势 || analysis.优势分析 || '',
|
||||
weaknesses: analysis.weaknesses || analysis.劣势 || analysis.劣势分析 || '',
|
||||
careerSuggestion: analysis.careerSuggestion || analysis.职业建议 || '',
|
||||
competitiveness: parseInt(analysis.competitiveness || analysis.竞争力评分 || 70, 10)
|
||||
};
|
||||
}
|
||||
|
||||
// 如果 analysis 是字符串,尝试解析
|
||||
const content = analysis?.content || analysis || '';
|
||||
if (typeof content === 'string' && content.includes('{') && content.includes('}')) {
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
|
||||
return {
|
||||
skillTags: parsed.skillTags || parsed.技能标签 || [],
|
||||
strengths: parsed.strengths || parsed.优势 || parsed.优势分析 || '',
|
||||
weaknesses: parsed.weaknesses || parsed.劣势 || parsed.劣势分析 || '',
|
||||
careerSuggestion: parsed.careerSuggestion || parsed.职业建议 || '',
|
||||
competitiveness: parsed.competitiveness || parsed.竞争力评分 || 70
|
||||
competitiveness: parseInt(parsed.competitiveness || parsed.竞争力评分 || 70, 10)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法解析JSON,尝试从文本中提取信息
|
||||
const skillTagsMatch = content.match(/技能标签[::](.*?)(?:\n|$)/);
|
||||
const strengthsMatch = content.match(/优势[分析]*[::](.*?)(?:\n|劣势)/s);
|
||||
const weaknessesMatch = content.match(/劣势[分析]*[::](.*?)(?:\n|职业)/s);
|
||||
const suggestionMatch = content.match(/职业建议[::](.*?)(?:\n|竞争力)/s);
|
||||
const scoreMatch = content.match(/竞争力评分[::](\d+)/);
|
||||
if (typeof content === 'string') {
|
||||
const skillTagsMatch = content.match(/技能标签[::](.*?)(?:\n|$)/);
|
||||
const strengthsMatch = content.match(/优势[分析]*[::](.*?)(?:\n|劣势)/s);
|
||||
const weaknessesMatch = content.match(/劣势[分析]*[::](.*?)(?:\n|职业)/s);
|
||||
const suggestionMatch = content.match(/职业建议[::](.*?)(?:\n|竞争力)/s);
|
||||
const scoreMatch = content.match(/竞争力评分[::](\d+)/);
|
||||
|
||||
return {
|
||||
skillTags: skillTagsMatch ? skillTagsMatch[1].split(/[,,、]/).map(s => s.trim()) : [],
|
||||
strengths: strengthsMatch ? strengthsMatch[1].trim() : '',
|
||||
weaknesses: weaknessesMatch ? weaknessesMatch[1].trim() : '',
|
||||
careerSuggestion: suggestionMatch ? suggestionMatch[1].trim() : '',
|
||||
competitiveness: scoreMatch ? parseInt(scoreMatch[1]) : 70
|
||||
};
|
||||
return {
|
||||
skillTags: skillTagsMatch ? skillTagsMatch[1].split(/[,,、]/).map(s => s.trim()) : [],
|
||||
strengths: strengthsMatch ? strengthsMatch[1].trim() : '',
|
||||
weaknesses: weaknessesMatch ? weaknessesMatch[1].trim() : '',
|
||||
careerSuggestion: suggestionMatch ? suggestionMatch[1].trim() : '',
|
||||
competitiveness: scoreMatch ? parseInt(scoreMatch[1], 10) : 70
|
||||
};
|
||||
}
|
||||
|
||||
// 如果所有解析都失败,使用默认分析
|
||||
console.warn(`[简历管理] 无法解析AI分析结果,使用默认分析`);
|
||||
return this.get_default_analysis(resumeInfo);
|
||||
} catch (error) {
|
||||
console.error(`[简历管理] 解析AI分析结果失败:`, error);
|
||||
// 解析失败时使用默认分析
|
||||
@@ -378,7 +516,7 @@ ${resumeInfo.skillDescription}
|
||||
|
||||
// 二期规划:AI 分析暂时禁用,使用简单的文本匹配
|
||||
// const analysis = await aiService.analyzeResume(resumeText);
|
||||
|
||||
|
||||
// 使用简单的文本匹配提取技能
|
||||
const skills = this.extract_skills_from_desc(resumeData.skillDescription || resumeText);
|
||||
|
||||
@@ -505,7 +643,7 @@ ${resumeInfo.skillDescription}
|
||||
throw new Error('MQTT客户端未初始化');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 重新获取和分析
|
||||
const resumeData = await this.get_online_resume(sn_code, mqttClient);
|
||||
return await this.analyze_resume(sn_code, resumeData);
|
||||
|
||||
Reference in New Issue
Block a user