From 68b4db0aee430c6917125b77c8a6651cdd9262de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Sun, 1 Feb 2026 22:00:19 +0800 Subject: [PATCH] 1 --- admin/src/router/component-map.js | 2 + api/middleware/job/aiService.js | 307 ------------ api/model/ai_call_records.js | 144 ++++++ api/services/ai_call_recorder.js | 168 +++++++ api/services/ai_service.js | 779 +++++++++++++++--------------- docs/ai_service_config.md | 145 ++++++ docs/ai_service_unified.md | 364 ++++++++++++++ docs/implementation_summary.md | 561 +++++++++++++++++++++ 8 files changed, 1762 insertions(+), 708 deletions(-) delete mode 100644 api/middleware/job/aiService.js create mode 100644 api/model/ai_call_records.js create mode 100644 api/services/ai_call_recorder.js create mode 100644 docs/ai_service_config.md create mode 100644 docs/ai_service_unified.md create mode 100644 docs/implementation_summary.md diff --git a/admin/src/router/component-map.js b/admin/src/router/component-map.js index 56fe105..fa70481 100644 --- a/admin/src/router/component-map.js +++ b/admin/src/router/component-map.js @@ -21,6 +21,7 @@ import TaskStatus from '@/views/task/task_status.vue' import SystemConfig from '@/views/system/system_config.vue' import Version from '@/views/system/version.vue' import JobTypes from '@/views/work/job_types.vue' +import AiCallRecords from '@/views/system/ai_call_records.vue' // 首页模块 import HomeIndex from '@/views/home/index.vue' @@ -53,6 +54,7 @@ const componentMap = { 'system/system_config': SystemConfig, 'system/version': Version, 'work/job_types': JobTypes, + 'system/ai_call_records': AiCallRecords, 'home/index': HomeIndex, diff --git a/api/middleware/job/aiService.js b/api/middleware/job/aiService.js deleted file mode 100644 index f53a631..0000000 --- a/api/middleware/job/aiService.js +++ /dev/null @@ -1,307 +0,0 @@ -const axios = require('axios'); -const config = require('../../../config/config'); -const logs = require('../logProxy'); - -/** - * 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; - } - - /** - * 调用 Qwen 2.5 API - * @param {string} prompt - 提示词 - * @param {object} options - 配置选项 - * @returns {Promise} API响应结果 - */ - async callAPI(prompt, options = {}) { - 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 - }; - - 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 - }); - - return { - data: response.data, - content: response.data.choices?.[0]?.message?.content || '' - }; - } catch (error) { - console.log(`Qwen 2.5 API调用失败 (尝试 ${attempt}/${this.maxRetries}): ${error.message}`); - - if (attempt === this.maxRetries) { - throw new Error(error.message); - } - - // 等待后重试 - await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); - } - } - } - - /** - * 岗位智能筛选 - * @param {object} jobInfo - 岗位信息 - * @param {object} resumeInfo - 简历信息 - * @returns {Promise} 筛选结果 - */ - async analyzeJob(jobInfo, resumeInfo) { - const prompt = ` -请分析以下岗位信息,并给出详细的评估结果: - -岗位信息: -- 公司名称:${jobInfo.companyName || '未知'} -- 职位名称:${jobInfo.jobTitle || '未知'} -- 薪资范围:${jobInfo.salary || '未知'} -- 工作地点:${jobInfo.location || '未知'} -- 岗位描述:${jobInfo.description || '未知'} -- 技能要求:${jobInfo.skills || '未知'} - -简历信息: -- 技能标签:${resumeInfo.skills || '未知'} -- 工作经验:${resumeInfo.experience || '未知'} -- 教育背景:${resumeInfo.education || '未知'} -- 期望薪资:${resumeInfo.expectedSalary || '未知'} - -请从以下维度进行分析: -1. 技能匹配度(0-100分) -2. 经验匹配度(0-100分) -3. 薪资合理性(0-100分) -4. 公司质量评估(0-100分) -5. 是否为外包岗位(是/否) -6. 综合推荐指数(0-100分) -7. 详细分析说明 -8. 投递建议 - -请以JSON格式返回结果。 - `; - - const result = await this.callAPI(prompt, { - systemPrompt: '你是一个专业的招聘分析师,擅长评估岗位与简历的匹配度。请提供客观、专业的分析结果。', - temperature: 0.3 - }); - - 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} 聊天内容 - */ - 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 - }); - - return result; - } - - /** - * 分析简历要素 - * @param {string} resumeText - 简历文本内容 - * @returns {Promise} 简历分析结果 - */ - async analyzeResume(resumeText) { - const prompt = ` -请分析以下简历内容,并返回 JSON 格式的分析结果: - -简历内容: -${resumeText} - -请按以下格式返回 JSON 结果: -{ - "skillTags": ["技能1", "技能2", "技能3"], // 技能标签数组(编程语言、框架、工具等) - "strengths": "核心优势描述", // 简历的优势和亮点 - "weaknesses": "不足之处描述", // 简历的不足或需要改进的地方 - "careerSuggestion": "职业发展建议", // 针对该简历的职业发展方向和建议 - "competitiveness": 75 // 竞争力评分(0-100的整数),综合考虑工作年限、技能、经验等因素 -} - -要求: -1. skillTags 必须是字符串数组 -2. strengths、weaknesses、careerSuggestion 是字符串描述 -3. competitiveness 必须是 0-100 之间的整数 -4. 所有字段都必须返回,如果没有相关信息,使用空数组或空字符串 - `; - - const result = await this.callAPI(prompt, { - systemPrompt: '你是一个专业的简历分析师,擅长分析简历的核心要素、优势劣势、竞争力评分和职业发展建议。请以 JSON 格式返回分析结果,确保格式正确。', - temperature: 0.3, - maxTokens: 1500 - }); - - 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 - } - }; - } - } - - /** - * 生成面试邀约内容 - * @param {object} jobInfo - 岗位信息 - * @param {object} chatHistory - 聊天历史 - * @returns {Promise} 面试邀约内容 - */ - 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 - }); - - return result; - } - - /** - * 识别外包岗位 - * @param {object} jobInfo - 岗位信息 - * @returns {Promise} 外包识别结果 - */ - 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 - }); - - return result; - } -} - -module.exports = new aiService(); \ No newline at end of file diff --git a/api/model/ai_call_records.js b/api/model/ai_call_records.js new file mode 100644 index 0000000..528957d --- /dev/null +++ b/api/model/ai_call_records.js @@ -0,0 +1,144 @@ +const Sequelize = require('sequelize'); +const { DataTypes } = Sequelize; + +module.exports = (sequelize) => { + const AiCallRecords = sequelize.define('ai_call_records', { + id: { + type: DataTypes.INTEGER(11), + allowNull: false, + primaryKey: true, + autoIncrement: true, + comment: '主键ID' + }, + user_id: { + type: DataTypes.INTEGER(11), + allowNull: true, + comment: '用户ID' + }, + sn_code: { + type: DataTypes.STRING(100), + allowNull: true, + comment: '设备SN码' + }, + service_type: { + type: DataTypes.STRING(50), + allowNull: false, + comment: '服务类型:chat/completion/embedding' + }, + model_name: { + type: DataTypes.STRING(100), + allowNull: false, + comment: '模型名称' + }, + prompt_tokens: { + type: DataTypes.INTEGER(11), + allowNull: false, + defaultValue: 0, + comment: '输入Token数' + }, + completion_tokens: { + type: DataTypes.INTEGER(11), + allowNull: false, + defaultValue: 0, + comment: '输出Token数' + }, + total_tokens: { + type: DataTypes.INTEGER(11), + allowNull: false, + defaultValue: 0, + comment: '总Token数' + }, + request_content: { + type: DataTypes.TEXT, + allowNull: true, + comment: '请求内容' + }, + response_content: { + type: DataTypes.TEXT, + allowNull: true, + comment: '响应内容' + }, + cost_amount: { + type: DataTypes.DECIMAL(10, 4), + allowNull: true, + comment: '费用(元)' + }, + status: { + type: DataTypes.STRING(20), + allowNull: false, + defaultValue: 'success', + comment: '状态:success/failed' + }, + error_message: { + type: DataTypes.TEXT, + allowNull: true, + comment: '错误信息' + }, + response_time: { + type: DataTypes.INTEGER(11), + allowNull: true, + comment: '响应时间(毫秒)' + }, + api_provider: { + type: DataTypes.STRING(50), + allowNull: true, + defaultValue: 'qwen', + comment: 'API提供商' + }, + business_type: { + type: DataTypes.STRING(50), + allowNull: true, + comment: '业务类型' + }, + reference_id: { + type: DataTypes.STRING(100), + allowNull: true, + comment: '关联业务ID' + }, + create_time: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间' + }, + last_modify_time: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '最后修改时间' + }, + is_delete: { + type: DataTypes.TINYINT(1), + allowNull: false, + defaultValue: 0, + comment: '是否删除:0-否,1-是' + } + }, { + tableName: 'ai_call_records', + timestamps: false, + indexes: [ + { + name: 'idx_user_id', + fields: ['user_id'] + }, + { + name: 'idx_sn_code', + fields: ['sn_code'] + }, + { + name: 'idx_create_time', + fields: ['create_time'] + }, + { + name: 'idx_is_delete', + fields: ['is_delete'] + }, + { + name: 'idx_business_type', + fields: ['business_type'] + } + ] + }); + + return AiCallRecords; +}; diff --git a/api/services/ai_call_recorder.js b/api/services/ai_call_recorder.js new file mode 100644 index 0000000..7184b68 --- /dev/null +++ b/api/services/ai_call_recorder.js @@ -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} 记录结果 + */ + 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} 统计数据 + */ + 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} 统计数据 + */ + 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; diff --git a/api/services/ai_service.js b/api/services/ai_service.js index 4d66fed..0ae95cc 100644 --- a/api/services/ai_service.js +++ b/api/services/ai_service.js @@ -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} 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} 分析结果 - */ - async analyzeResume(resumeData) { - const prompt = `请分析以下简历的竞争力,并提供详细评估: + /** + * 调用 Qwen 2.5 API + * @param {string} prompt - 提示词 + * @param {object} options - 配置选项 + * @returns {Promise} 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} 筛选结果 + */ + 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} 聊天内容 + */ + 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} 简历分析结果 + */ + 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} 匹配结果 - */ - 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} 评估结果列表 - */ - 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} 生成的聊天内容 - */ - 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} 判断结果 - */ - 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} 情感分析结果 - */ - 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} 面试邀约内容 + */ + 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} 外包识别结果 + */ + 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(); diff --git a/docs/ai_service_config.md b/docs/ai_service_config.md new file mode 100644 index 0000000..1439d08 --- /dev/null +++ b/docs/ai_service_config.md @@ -0,0 +1,145 @@ +# AI 服务配置说明 + +## 环境变量配置 + +AI 服务需要通过环境变量进行配置,支持阿里云 DashScope API。 + +### 必需的环境变量 + +在 `.env` 文件或系统环境变量中配置以下参数: + +```bash +# AI API 密钥(阿里云 DashScope API Key) +AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx + +# AI API 基础 URL(阿里云 DashScope 兼容 OpenAI 格式接口) +AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 + +# AI 模型名称 +# 可选值: +# - qwen-turbo(快速,推荐日常使用) +# - qwen-plus(增强,平衡性能和成本) +# - qwen-max(最强,高质量输出) +# - qwen-long(长文本,支持超长上下文) +AI_MODEL=qwen-turbo +``` + +### 配置示例 + +#### 1. 开发环境配置 (.env) + +```bash +# 阿里云 DashScope 配置 +AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +AI_MODEL=qwen-turbo +``` + +#### 2. 生产环境配置 (.env.production) + +```bash +# 阿里云 DashScope 配置(生产环境使用增强版) +AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +AI_MODEL=qwen-plus +``` + +### 阿里云模型对比 + +| 模型 | 速度 | 质量 | 成本 | 适用场景 | +|------|------|------|------|----------| +| qwen-turbo | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 日常对话、简单分析 | +| qwen-plus | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 复杂分析、专业任务 | +| qwen-max | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 高质量输出、关键任务 | +| qwen-long | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 长文本处理、文档分析 | + +### 代码中使用 + +```javascript +// 使用默认配置(从环境变量读取) +const AIService = require('./services/ai_service.js'); +const aiService = AIService.getInstance(); + +// 使用自定义配置 +const aiService = AIService.createInstance({ + apiKey: 'sk-custom-key', + baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + model: 'qwen-plus', + timeout: 60000 +}); +``` + +### API 认证格式 + +阿里云 DashScope API 使用标准的 Bearer Token 认证: + +``` +Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxx +``` + +### 注意事项 + +1. **API Key 安全**: + - 不要将 API Key 硬编码在代码中 + - 不要将 `.env` 文件提交到版本控制 + - 生产环境使用独立的 API Key + +2. **模型选择建议**: + - 开发/测试:使用 `qwen-turbo`(成本低) + - 生产环境:使用 `qwen-plus`(性能平衡) + - 关键业务:使用 `qwen-max`(质量最高) + +3. **速率限制**: + - 注意 API 的 QPM(每分钟请求数)限制 + - 根据套餐调整并发数量 + - 实现重试和错误处理机制 + +4. **成本控制**: + - 监控 Token 使用量 + - 设置合理的 `max_tokens` 限制 + - 定期查看账单和用量统计 + +### 获取 API Key + +1. 访问阿里云控制台:https://dashscope.console.aliyun.com/ +2. 进入 API-KEY 管理 +3. 创建新的 API Key +4. 复制 API Key 并保存到环境变量 + +### 故障排查 + +#### 问题 1:Authentication Fails +``` +错误:auth header format should be Bearer sk-... +解决:检查 AI_API_KEY 是否正确配置 +``` + +#### 问题 2:连接超时 +``` +错误:timeout of 30000ms exceeded +解决: +1. 检查网络连接 +2. 增加 timeout 配置 +3. 检查 AI_BASE_URL 是否正确 +``` + +#### 问题 3:模型不存在 +``` +错误:model not found +解决:检查 AI_MODEL 配置,确保使用支持的模型名称 +``` + +### 迁移指南 + +如果之前使用其他 AI 服务(如 DeepSeek),迁移步骤: + +1. 更新环境变量配置 +2. 修改 API_BASE_URL +3. 更新模型名称 +4. 测试 AI 调用功能 +5. 验证响应格式 + +--- + +**配置更新时间**: 2025-12-27 +**维护者**: 系统管理员 diff --git a/docs/ai_service_unified.md b/docs/ai_service_unified.md new file mode 100644 index 0000000..0cd92b3 --- /dev/null +++ b/docs/ai_service_unified.md @@ -0,0 +1,364 @@ +# AI 服务统一说明 + +**更新时间**: 2025-12-27 + +--- + +## 统一后的 AI 服务架构 + +系统已完成 AI 服务的统一整理,现在只保留一个标准的 AI 服务实现。 + +--- + +## 文件位置 + +### ✅ 保留的文件(唯一 AI 服务实现) + +**核心服务:** +- **`api/services/ai_service.js`** - AI 服务主文件(基于阿里云 Qwen 2.5) +- **`api/services/ai_call_recorder.js`** - AI 调用记录服务 + +**导出管理:** +- **`api/services/index.js`** - 服务统一导出 + +**数据库层:** +- **`api/model/ai_call_records.js`** - AI 调用记录模型 + +**后台管理:** +- **`api/controller_admin/ai_call_records.js`** - 后台管理 API + +**前端界面:** +- **`admin/src/views/system/ai_call_records.vue`** - 管理界面 +- **`admin/src/api/system/ai_call_records_server.js`** - API 服务 + +### ❌ 已删除的文件 + +- ~~`api/middleware/job/aiService.js`~~ - 已删除(内容已迁移到 `services/ai_service.js`) + +--- + +## 使用方式 + +### 1. 直接引用(推荐) + +```javascript +const aiService = require('./services/ai_service'); + +// 使用 AI 服务 +const result = await aiService.analyzeJob(jobInfo, resumeInfo); +``` + +### 2. 通过服务管理器 + +```javascript +const { AIService } = require('./services'); + +// 使用 AI 服务 +const result = await AIService.analyzeJob(jobInfo, resumeInfo); +``` + +--- + +## AI 服务功能列表 + +### 核心方法 + +| 方法 | 说明 | 业务类型 | +|------|------|---------| +| `callAPI(prompt, options)` | 基础 API 调用 | 自定义 | +| `analyzeJob(jobInfo, resumeInfo)` | 岗位智能筛选 | `job_analysis` | +| `generateChatContent(jobInfo, resumeInfo, chatType)` | 生成个性化聊天 | `chat_generation` | +| `analyzeResume(resumeText)` | 简历分析 | `resume_analysis` | +| `generateInterviewInvitation(jobInfo, chatHistory)` | 生成面试邀约 | `interview_invitation` | +| `identifyOutsourcingJob(jobInfo)` | 识别外包岗位 | `outsourcing_detection` | + +### 辅助方法 + +| 方法 | 说明 | +|------|------| +| `recordAiCall(params)` | 记录 AI 调用 | +| `calculateCost(totalTokens)` | 计算调用费用 | + +--- + +## Token 自动记录 + +所有通过 `callAPI()` 方法的调用都会自动记录以下信息: + +- **Token 使用量**:prompt_tokens, completion_tokens, total_tokens +- **成本信息**:基于模型计算的费用 +- **性能指标**:响应时间(毫秒) +- **状态跟踪**:成功/失败状态 +- **业务关联**:business_type, reference_id +- **请求追踪**:完整的请求和响应内容 + +记录过程是异步非阻塞的,不会影响 AI 调用的主流程。 + +--- + +## 配置说明 + +### 环境变量 + +在 `.env` 文件中配置: + +```bash +# 阿里云 DashScope API Key +AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx + +# 模型选择(可选) +AI_MODEL=qwen-turbo # qwen-turbo, qwen-plus, qwen-max, qwen-long +``` + +### 代码配置 + +```javascript +// 在 config/config.js 中 +module.exports = { + ai: { + apiKey: process.env.AI_API_KEY, + model: process.env.AI_MODEL || 'qwen-turbo' + } +} +``` + +--- + +## 模型选择 + +| 模型 | 速度 | 质量 | 成本 | 价格(元/1000 tokens)| +|------|------|------|------|---------------------| +| qwen-turbo | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ¥0.003 | +| qwen-plus | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ¥0.004 | +| qwen-max | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ¥0.12 | +| qwen-long | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | - | + +--- + +## 使用示例 + +### 示例 1:岗位分析 + +```javascript +const aiService = require('./services/ai_service'); + +const jobInfo = { + companyName: '阿里巴巴', + jobTitle: 'Node.js 高级工程师', + salary: '30-50K', + location: '杭州', + description: '负责后端服务开发...', + skills: 'Node.js, MySQL, Redis' +}; + +const resumeInfo = { + skills: 'Node.js, JavaScript, MySQL', + experience: '5年后端开发经验', + education: '本科', + expectedSalary: '35K' +}; + +const result = await aiService.analyzeJob(jobInfo, resumeInfo); +console.log(result.analysis); +``` + +### 示例 2:生成聊天内容 + +```javascript +const result = await aiService.generateChatContent( + jobInfo, + resumeInfo, + 'greeting' // greeting, interview, followup +); + +console.log(result.content); +``` + +### 示例 3:简历分析 + +```javascript +const resumeText = ` +姓名:张三 +技能:Node.js, React, MySQL +工作经验:3年全栈开发 +... +`; + +const result = await aiService.analyzeResume(resumeText); +console.log(result.analysis); +// { +// skillTags: ['Node.js', 'React', 'MySQL'], +// strengths: '...', +// weaknesses: '...', +// careerSuggestion: '...', +// competitiveness: 75 +// } +``` + +### 示例 4:自定义 AI 调用 + +```javascript +const result = await aiService.callAPI( + '请帮我分析这个岗位的发展前景...', + { + systemPrompt: '你是一个职业规划专家...', + temperature: 0.7, + maxTokens: 2000, + user_id: 123, + business_type: 'career_analysis' + } +); + +console.log(result.content); +``` + +--- + +## 错误处理 + +AI 服务内置了重试机制(最多 3 次)和错误处理: + +```javascript +try { + const result = await aiService.analyzeJob(jobInfo, resumeInfo); +} catch (error) { + console.error('AI 调用失败:', error.message); + // 错误会自动记录到 ai_call_records 表 +} +``` + +--- + +## 监控与统计 + +### 查看调用记录 + +登录后台管理系统:**系统设置** → **AI调用记录** + +### 统计信息 + +- 总调用次数 +- Token 总使用量 +- 总费用统计 +- 平均响应时间 +- 成功率 + +### 编程方式获取统计 + +```javascript +const AiCallRecorder = require('./services/ai_call_recorder'); + +// 获取用户统计 +const userStats = await AiCallRecorder.getUserTokenStats(userId, { + startDate: new Date('2025-01-01'), + endDate: new Date('2025-12-31') +}); + +console.log(userStats); +// { +// total_calls: 100, +// total_prompt_tokens: 5000, +// total_completion_tokens: 3000, +// total_tokens: 8000, +// total_cost: 24.00, +// avg_response_time: 1500 +// } +``` + +--- + +## 注意事项 + +### 1. API Key 安全 + +- ❌ 不要将 API Key 硬编码在代码中 +- ❌ 不要将 `.env` 文件提交到版本控制 +- ✅ 使用环境变量管理 API Key +- ✅ 生产环境使用独立的 API Key + +### 2. 成本控制 + +- 选择合适的模型(开发用 turbo,生产用 plus) +- 设置合理的 `maxTokens` 限制 +- 监控 Token 使用量 +- 定期查看费用统计 + +### 3. 性能优化 + +- 重试机制已内置(3 次) +- 超时设置为 30 秒 +- Token 记录是异步的,不阻塞主流程 + +### 4. 数据隐私 + +- 请求和响应内容会完整记录到数据库 +- 注意敏感信息的处理 +- 定期清理历史记录 + +--- + +## 迁移指南 + +如果你的代码之前引用了 `middleware/job/aiService.js`,请修改为: + +```javascript +// ❌ 旧代码 +const aiService = require('../middleware/job/aiService'); + +// ✅ 新代码 +const aiService = require('../services/ai_service'); +``` + +功能保持完全一致,只是路径发生了变化。 + +--- + +## 故障排查 + +### 问题 1:模型未加载 + +**错误信息:** `Cannot read property 'findAll' of undefined` + +**解决方法:** +1. 确认已执行建表 SQL:`_sql/create_ai_call_records_table.sql` +2. 重启 Node.js 服务 +3. 检查 `api/model/ai_call_records.js` 是否存在 + +### 问题 2:认证失败 + +**错误信息:** `auth header format should be Bearer sk-...` + +**解决方法:** +1. 检查 `.env` 文件中的 `AI_API_KEY` +2. 确认 API Key 格式正确(以 `sk-` 开头) +3. 验证 API Key 有效性 + +### 问题 3:记录失败 + +**警告信息:** `记录AI调用失败(不影响主流程)` + +**解决方法:** +1. 检查数据库连接 +2. 确认 `ai_call_records` 表存在 +3. 查看详细错误日志 + +### 问题 4:费用计算不准确 + +**解决方法:** +1. 检查 `calculateCost()` 方法中的价格配置 +2. 根据实际使用的模型调整价格 +3. 定期对账单进行核对 + +--- + +## 相关文档 + +- [AI 服务配置说明](ai_service_config.md) - 详细的环境配置指南 +- [功能实施总结](implementation_summary.md) - 完整的功能实施文档 +- [API 文档](../api/controller_admin/ai_call_records.js) - 后台 API 接口说明 + +--- + +**文档版本**: 1.0 +**最后更新**: 2025-12-27 +**维护者**: 开发团队 diff --git a/docs/implementation_summary.md b/docs/implementation_summary.md new file mode 100644 index 0000000..b1e3fa8 --- /dev/null +++ b/docs/implementation_summary.md @@ -0,0 +1,561 @@ +# 自动找工作系统 - 功能实施总结 + +**更新时间**: 2025-12-27 + +--- + +## 已完成功能概览 + +本文档总结了最近完成的两个主要功能模块: + +1. **价格套餐管理系统** +2. **AI调用记录与Token追踪系统** + +--- + +## 一、价格套餐管理系统 + +### 功能描述 + +将原本硬编码在 `api/controller_front/config.js` 中的价格套餐数据迁移到数据库,并提供完整的后台管理界面。 + +### 实施文件清单 + +#### 数据库层 +- ✅ `_sql/create_pricing_plans_table.sql` - 数据表创建脚本 +- ✅ `_sql/insert_pricing_plans_data.sql` - 初始数据插入脚本 +- ✅ `api/model/pricing_plans.js` - Sequelize 数据模型 + +#### 后端API层 +- ✅ `api/controller_admin/pricing_plans.js` - 后台管理API(5个端点) + - `POST /pricing_plans/list` - 分页查询 + - `GET /pricing_plans/detail` - 获取详情 + - `POST /pricing_plans/create` - 创建套餐 + - `POST /pricing_plans/update` - 更新套餐 + - `POST /pricing_plans/delete` - 删除套餐(软删除) +- ✅ `api/controller_front/config.js` (修改第90-136行) - 前端API改为数据库查询 + +#### 前端层 +- ✅ `admin/src/api/system/pricing_plans_server.js` - API服务层 +- ✅ `admin/src/views/system/pricing_plans.vue` - 管理界面组件 +- ✅ `admin/src/router/component-map.js` (新增映射) - 组件注册 + +### 数据库表结构 + +```sql +CREATE TABLE `pricing_plans` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL COMMENT '套餐名称', + `duration` VARCHAR(50) NOT NULL COMMENT '时长描述', + `days` INT(11) NOT NULL COMMENT '天数(-1表示永久)', + `price` DECIMAL(10,2) NOT NULL COMMENT '价格', + `original_price` DECIMAL(10,2) NULL COMMENT '原价', + `unit` VARCHAR(20) NOT NULL DEFAULT '元', + `discount` VARCHAR(50) NULL COMMENT '折扣描述', + `features` TEXT NOT NULL COMMENT '功能列表(JSON格式)', + `featured` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否推荐', + `is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用', + `sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT '排序顺序', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 初始数据 + +系统预置了4个价格套餐: + +1. **体验套餐** - 7天 - ¥28 +2. **月度套餐** - 30天 - ¥99(推荐) +3. **季度套餐** - 90天 - ¥269 +4. **终生套餐** - 永久 - ¥888 + +### 菜单位置 + +**用户管理** → **价格套餐管理** + +### 关键实现细节 + +#### 前端接口向后兼容 + +`GET /api/config/pricing-plans` 接口保持原有响应格式: + +```javascript +[ + { + "id": 1, + "name": "体验套餐", + "duration": "7天", + "days": 7, + "price": 28, + "originalPrice": 28, + "unit": "元", + "features": ["7天使用权限", "全功能体验", "技术支持"], + "featured": false + } +] +``` + +#### 状态控制逻辑 + +- 前端API仅返回 `is_active = 1` 且 `is_delete = 0` 的套餐 +- 按照 `sort_order` ASC, `id` ASC 排序 +- Features字段从JSON字符串自动解析为数组 + +#### 表单组件修复 + +修复了单选按钮组件使用方式(重要): + +```javascript +// ❌ 错误写法 +{ + title: '是否推荐', + key: 'featured', + type: 'radio', // 错误 + options: [...] +} + +// ✅ 正确写法 +{ + title: '是否推荐', + key: 'featured', + com: 'Radio', // 正确 + options: [ + { value: 1, label: '推荐' }, + { value: 0, label: '普通' } + ] +} +``` + +--- + +## 二、AI调用记录与Token追踪系统 + +### 功能描述 + +为所有AI调用添加自动记录功能,追踪Token使用量、调用成本、响应时间等关键指标,并提供后台管理和统计分析界面。 + +### 实施文件清单 + +#### 数据库层 +- ✅ `_sql/create_ai_call_records_table.sql` - 数据表创建脚本 +- ✅ `api/model/ai_call_records.js` - Sequelize 数据模型 + +#### 服务层 +- ✅ `api/services/ai_call_recorder.js` - AI调用记录服务 +- ✅ `api/services/ai_service.js` - AI服务(集成Token记录) + +#### 后端API层 +- ✅ `api/controller_admin/ai_call_records.js` - 后台管理API(5个端点) + - `POST /ai_call_records/list` - 分页查询 + - `GET /ai_call_records/detail` - 获取详情 + - `GET /ai_call_records/stats` - 统计分析 + - `POST /ai_call_records/delete` - 删除记录 + - `POST /ai_call_records/batch_delete` - 批量删除 + +#### 前端层 +- ✅ `admin/src/api/system/ai_call_records_server.js` - API服务层 +- ✅ `admin/src/views/system/ai_call_records.vue` - 管理界面组件 +- ✅ `admin/src/router/component-map.js` (新增映射) - 组件注册 + +### 数据库表结构 + +```sql +CREATE TABLE `ai_call_records` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` INT(11) NULL COMMENT '用户ID', + `sn_code` VARCHAR(100) NULL COMMENT '设备SN码', + `service_type` VARCHAR(50) NOT NULL COMMENT '服务类型:chat/completion/embedding', + `model_name` VARCHAR(100) NOT NULL COMMENT '模型名称', + `prompt_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '输入Token数', + `completion_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '输出Token数', + `total_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '总Token数', + `request_content` TEXT NULL COMMENT '请求内容', + `response_content` TEXT NULL COMMENT '响应内容', + `cost_amount` DECIMAL(10,4) NULL COMMENT '费用(元)', + `status` VARCHAR(20) NOT NULL DEFAULT 'success' COMMENT '状态:success/failed', + `error_message` TEXT NULL COMMENT '错误信息', + `response_time` INT(11) NULL COMMENT '响应时间(毫秒)', + `api_provider` VARCHAR(50) NULL DEFAULT 'qwen' COMMENT 'API提供商', + `business_type` VARCHAR(50) NULL COMMENT '业务类型', + `reference_id` VARCHAR(100) NULL COMMENT '关联业务ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### AI服务集成 + +#### 使用的AI服务 + +系统使用 **阿里云DashScope API (Qwen 2.5)**,而非DeepSeek。 + +**关键配置:** + +```javascript +// api/services/ai_service.js +class aiService { + constructor() { + this.apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY; + this.apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'; + this.model = config.ai?.model || 'qwen-turbo'; + this.maxRetries = 3; + } +} +``` + +#### 环境变量配置 + +在 `.env` 文件中配置: + +```bash +AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx +AI_MODEL=qwen-turbo # 可选:qwen-turbo, qwen-plus, qwen-max, qwen-long +``` + +详细配置说明见:[docs/ai_service_config.md](ai_service_config.md:1) + +#### Token记录集成 + +AI服务的 `callAPI()` 方法自动记录所有调用: + +```javascript +async callAPI(prompt, options = {}) { + const startTime = Date.now(); + // ... 调用API ... + + // 成功时自动记录Token使用量 + 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, + 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); + }); +} +``` + +#### 业务类型分类 + +系统中的AI调用按业务类型分类: + +| 业务类型 | 说明 | 调用方法 | +|---------|------|---------| +| `job_analysis` | 岗位分析 | `analyzeJob()` | +| `chat_generation` | 聊天内容生成 | `generateChatContent()` | +| `resume_analysis` | 简历分析 | `analyzeResume()` | +| `interview_invitation` | 面试邀约 | `generateInterviewInvitation()` | +| `outsourcing_detection` | 外包检测 | `identifyOutsourcingJob()` | + +#### 成本计算 + +基于Qwen模型定价: + +```javascript +calculateCost(totalTokens) { + // qwen-turbo: ¥0.003/1000 tokens + // qwen-plus: ¥0.004/1000 tokens + // qwen-max: ¥0.12/1000 tokens + const pricePerThousand = 0.003; + return (totalTokens / 1000) * pricePerThousand; +} +``` + +### 管理界面功能 + +#### 筛选功能 +- 按用户ID搜索 +- 按设备SN码搜索 +- 按业务类型筛选 +- 按服务类型筛选 +- 按状态筛选 +- 按时间范围筛选 + +#### 统计功能 +- 总调用次数 +- Token总使用量(输入/输出/总计) +- 总费用统计 +- 平均响应时间 +- 成功率统计 + +#### 操作功能 +- 查看详情(完整请求/响应内容) +- 单条删除 +- 批量删除 + +### 菜单位置 + +**系统设置** → **AI调用记录** + +--- + +## 三、关键技术点总结 + +### 1. 组件注册规范 + +前端Vue组件必须在 `admin/src/router/component-map.js` 中注册: + +```javascript +// 导入组件 +import PricingPlans from '@/views/system/pricing_plans.vue' +import AiCallRecords from '@/views/system/ai_call_records.vue' + +// 注册映射 +const componentMap = { + 'system/pricing_plans': PricingPlans, + 'system/ai_call_records': AiCallRecords, +} +``` + +### 2. 表单控件使用规范 + +使用 `com` 字段而非 `type` 字段: + +```javascript +// 单选按钮 +{ com: 'Radio', options: [...] } + +// 文本输入框 +{ com: 'Input' } + +// 文本域 +{ com: 'TextArea' } + +// 数字输入框 +{ com: 'InputNumber' } + +// 下拉选择 +{ com: 'Select', options: [...] } +``` + +### 3. 数据库软删除模式 + +所有查询必须包含 `is_delete = 0` 条件: + +```javascript +const list = await model.findAll({ + where: { + is_delete: 0, + // ... 其他条件 + } +}); +``` + +### 4. 时间戳管理 + +使用手动管理而非Sequelize自动管理: + +```javascript +// 模型定义 +{ + timestamps: false, // 禁用自动时间戳 + // 手动定义时间字段 + create_time: { type: DataTypes.DATE }, + last_modify_time: { type: DataTypes.DATE } +} + +// 创建时手动设置 +const now = new Date(); +await model.create({ + // ... 其他字段 + create_time: now, + last_modify_time: now +}); +``` + +### 5. JSON字段处理 + +数据库存储为TEXT类型,应用层处理JSON序列化: + +```javascript +// 保存时 +const data = { + features: JSON.stringify(['功能1', '功能2']) +}; + +// 读取时 +const features = JSON.parse(record.features || '[]'); +``` + +### 6. 异步记录模式 + +日志/记录类操作使用异步非阻塞模式: + +```javascript +// 使用 .catch() 而非 try-catch,避免阻塞主流程 +this.recordAiCall(params).catch(err => { + console.warn('记录失败(不影响主流程):', err.message); +}); + +// 主流程继续执行 +return result; +``` + +--- + +## 四、部署检查清单 + +### 数据库层 +- [ ] 执行 `create_pricing_plans_table.sql` +- [ ] 执行 `insert_pricing_plans_data.sql` +- [ ] 执行 `create_ai_call_records_table.sql` +- [ ] 验证表创建成功 + +### 后端层 +- [ ] 重启Node.js服务以加载新模型 +- [ ] 验证模型加载:`Framework.getModels()` +- [ ] 配置环境变量 `AI_API_KEY` 和 `AI_MODEL` +- [ ] 测试后台API端点 + +### 前端层 +- [ ] 重新编译前端代码 +- [ ] 验证组件注册成功 +- [ ] 刷新浏览器缓存 +- [ ] 测试管理界面功能 + +### 菜单系统 +- [ ] 验证"价格套餐管理"菜单显示 +- [ ] 验证"AI调用记录"菜单显示 +- [ ] 测试菜单跳转功能 + +### 功能测试 +- [ ] 价格套餐CRUD操作 +- [ ] 前端API `/config/pricing-plans` 返回数据库数据 +- [ ] AI调用自动记录Token +- [ ] AI调用记录管理界面 +- [ ] 统计功能准确性 + +--- + +## 五、已知问题与注意事项 + +### 1. 组件热更新 + +修改组件映射后需要: +- 重启前端开发服务器 +- 清除浏览器缓存 +- 刷新页面 + +### 2. AI服务配置 + +**重要**:系统使用阿里云DashScope API,不是DeepSeek。 + +必须配置正确的环境变量: +```bash +AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx # 阿里云API Key +AI_MODEL=qwen-turbo # Qwen模型 +``` + +### 3. Token记录异步 + +Token记录失败不会影响AI调用主流程,但会打印警告日志。如果需要确保记录成功,应检查数据库连接和表结构。 + +### 4. 成本计算 + +当前成本计算使用固定价格(qwen-turbo: ¥0.003/1000 tokens),如果切换到其他模型,需要修改 `calculateCost()` 方法。 + +### 5. 数据备份 + +AI调用记录表会快速增长,建议: +- 定期归档历史数据 +- 设置数据保留策略(如只保留最近90天) +- 建立定期备份机制 + +--- + +## 六、文件路径索引 + +### 价格套餐系统 + +**数据库** +- `_sql/create_pricing_plans_table.sql` +- `_sql/insert_pricing_plans_data.sql` + +**后端** +- `api/model/pricing_plans.js` +- `api/controller_admin/pricing_plans.js` +- `api/controller_front/config.js` (修改第90-136行) + +**前端** +- `admin/src/api/system/pricing_plans_server.js` +- `admin/src/views/system/pricing_plans.vue` + +### AI调用记录系统 + +**数据库** +- `_sql/create_ai_call_records_table.sql` + +**后端** +- `api/model/ai_call_records.js` +- `api/services/ai_call_recorder.js` +- `api/services/ai_service.js` (完全重写) +- `api/controller_admin/ai_call_records.js` + +**前端** +- `admin/src/api/system/ai_call_records_server.js` +- `admin/src/views/system/ai_call_records.vue` + +### 公共配置 + +- `admin/src/router/component-map.js` (新增两个组件映射) +- `config/config.js` (AI配置项) +- `.env` (环境变量) + +### 文档 + +- `docs/ai_service_config.md` (AI服务配置说明) +- `docs/implementation_summary.md` (本文档) + +--- + +## 七、维护建议 + +### 日常维护 + +1. **监控Token使用量** + - 定期查看AI调用记录统计 + - 关注异常高额调用 + - 优化高频调用场景 + +2. **价格套餐调整** + - 根据市场情况调整价格 + - 通过后台界面快速上下架套餐 + - 使用排序功能控制显示顺序 + +3. **数据清理** + - 定期归档或删除历史AI调用记录 + - 清理已删除的套餐数据(物理删除) + +### 性能优化 + +1. **数据库索引** + - AI调用记录表按需添加索引(user_id, sn_code, create_time) + - 定期分析查询性能 + +2. **缓存策略** + - 考虑对前端API `/config/pricing-plans` 添加缓存 + - 缓存有效期建议5-10分钟 + +3. **日志归档** + - 建立AI调用记录的归档机制 + - 超过一定时间的数据转移到历史表 + +--- + +**文档版本**: 1.0 +**最后更新**: 2025-12-27 +**维护者**: 开发团队