This commit is contained in:
张成
2025-12-27 20:14:40 +08:00
parent 43382668a3
commit 43f7884e52
14 changed files with 1818 additions and 21 deletions

View File

@@ -0,0 +1,317 @@
/**
* AI调用记录管理API - 后台管理
* 提供AI调用记录的查询和统计功能
*/
const Framework = require("../../framework/node-core-framework.js");
module.exports = {
/**
* @swagger
* /admin_api/ai_call_records/list:
* post:
* summary: 获取AI调用记录列表
* description: 分页获取所有AI调用记录
* tags: [后台-AI调用记录管理]
*/
'POST /ai_call_records/list': async (ctx) => {
try {
const models = Framework.getModels();
const { ai_call_records, op } = models;
const body = ctx.getBody();
// 获取分页参数
const { limit, offset } = ctx.getPageSize();
// 构建查询条件
const where = { is_delete: 0 };
// 搜索条件
if (body.seachOption) {
const { key, value, status, service_type, model_name, api_provider, business_type, user_id, sn_code } = body.seachOption;
// 关键字搜索
if (value && key) {
if (key === 'user_id' || key === 'reference_id') {
where[key] = value;
} else if (key === 'sn_code') {
where.sn_code = { [op.like]: `%${value}%` };
}
}
// 状态筛选
if (status) {
where.status = status;
}
// 服务类型筛选
if (service_type) {
where.service_type = service_type;
}
// 模型名称筛选
if (model_name) {
where.model_name = model_name;
}
// API提供商筛选
if (api_provider) {
where.api_provider = api_provider;
}
// 业务类型筛选
if (business_type) {
where.business_type = business_type;
}
// 用户ID筛选
if (user_id) {
where.user_id = user_id;
}
// 设备序列号筛选
if (sn_code) {
where.sn_code = sn_code;
}
// 日期范围筛选
if (body.seachOption.start_date && body.seachOption.end_date) {
where.create_time = {
[op.between]: [new Date(body.seachOption.start_date), new Date(body.seachOption.end_date)]
};
}
}
const result = await ai_call_records.findAndCountAll({
where,
limit,
offset,
order: [['create_time', 'DESC'], ['id', 'DESC']],
attributes: [
'id', 'user_id', 'sn_code', 'service_type', 'model_name',
'prompt_tokens', 'completion_tokens', 'total_tokens',
'cost_amount', 'status', 'response_time', 'api_provider',
'business_type', 'reference_id', 'create_time'
]
});
return ctx.success(result);
} catch (error) {
console.error('获取AI调用记录列表失败:', error);
return ctx.fail('获取AI调用记录列表失败: ' + error.message);
}
},
/**
* @swagger
* /admin_api/ai_call_records/detail:
* get:
* summary: 获取AI调用记录详情
* description: 根据ID获取AI调用记录详细信息包含请求和响应内容
* tags: [后台-AI调用记录管理]
*/
'GET /ai_call_records/detail': async (ctx) => {
try {
const models = Framework.getModels();
const { ai_call_records } = models;
const { id } = ctx.getQuery();
if (!id) {
return ctx.fail('记录ID不能为空');
}
const record = await ai_call_records.findOne({
where: { id, is_delete: 0 }
});
if (!record) {
return ctx.fail('记录不存在');
}
return ctx.success(record);
} catch (error) {
console.error('获取AI调用记录详情失败:', error);
return ctx.fail('获取AI调用记录详情失败: ' + error.message);
}
},
/**
* @swagger
* /admin_api/ai_call_records/stats:
* post:
* summary: 获取AI调用统计数据
* description: 统计Token使用量、调用次数、费用等
* tags: [后台-AI调用记录管理]
*/
'POST /ai_call_records/stats': async (ctx) => {
try {
const models = Framework.getModels();
const { ai_call_records, op } = models;
const body = ctx.getBody();
const where = { is_delete: 0, status: 'success' };
// 构建查询条件
if (body.user_id) {
where.user_id = body.user_id;
}
if (body.sn_code) {
where.sn_code = body.sn_code;
}
if (body.business_type) {
where.business_type = body.business_type;
}
if (body.start_date && body.end_date) {
where.create_time = {
[op.between]: [new Date(body.start_date), new Date(body.end_date)]
};
}
const records = await ai_call_records.findAll({ where });
// 计算统计数据
const stats = {
total_calls: records.length,
total_prompt_tokens: 0,
total_completion_tokens: 0,
total_tokens: 0,
total_cost: 0,
avg_response_time: 0,
by_model: {},
by_service_type: {},
by_status: { success: 0, failed: 0, timeout: 0 }
};
let totalResponseTime = 0;
let responseTimeCount = 0;
records.forEach(record => {
// Token统计
stats.total_prompt_tokens += record.prompt_tokens || 0;
stats.total_completion_tokens += record.completion_tokens || 0;
stats.total_tokens += record.total_tokens || 0;
stats.total_cost += parseFloat(record.cost_amount || 0);
// 响应时间统计
if (record.response_time) {
totalResponseTime += record.response_time;
responseTimeCount++;
}
// 按模型统计
if (!stats.by_model[record.model_name]) {
stats.by_model[record.model_name] = {
count: 0,
total_tokens: 0,
total_cost: 0
};
}
stats.by_model[record.model_name].count++;
stats.by_model[record.model_name].total_tokens += record.total_tokens || 0;
stats.by_model[record.model_name].total_cost += parseFloat(record.cost_amount || 0);
// 按服务类型统计
if (!stats.by_service_type[record.service_type]) {
stats.by_service_type[record.service_type] = {
count: 0,
total_tokens: 0
};
}
stats.by_service_type[record.service_type].count++;
stats.by_service_type[record.service_type].total_tokens += record.total_tokens || 0;
});
// 计算平均响应时间
if (responseTimeCount > 0) {
stats.avg_response_time = Math.round(totalResponseTime / responseTimeCount);
}
// 查询失败和超时的记录
const failedCount = await ai_call_records.count({
where: { ...where, status: 'failed', is_delete: 0 }
});
const timeoutCount = await ai_call_records.count({
where: { ...where, status: 'timeout', is_delete: 0 }
});
stats.by_status.success = records.length;
stats.by_status.failed = failedCount;
stats.by_status.timeout = timeoutCount;
return ctx.success(stats);
} catch (error) {
console.error('获取AI调用统计失败:', error);
return ctx.fail('获取AI调用统计失败: ' + error.message);
}
},
/**
* @swagger
* /admin_api/ai_call_records/delete:
* post:
* summary: 删除AI调用记录
* description: 软删除指定的AI调用记录
* tags: [后台-AI调用记录管理]
*/
'POST /ai_call_records/delete': async (ctx) => {
try {
const models = Framework.getModels();
const { ai_call_records } = models;
const { id } = ctx.getBody();
if (!id) {
return ctx.fail('记录ID不能为空');
}
const record = await ai_call_records.findOne({
where: { id, is_delete: 0 }
});
if (!record) {
return ctx.fail('记录不存在');
}
// 软删除
await ai_call_records.update(
{ is_delete: 1 },
{ where: { id } }
);
return ctx.success({ message: 'AI调用记录删除成功' });
} catch (error) {
console.error('删除AI调用记录失败:', error);
return ctx.fail('删除AI调用记录失败: ' + error.message);
}
},
/**
* @swagger
* /admin_api/ai_call_records/batch_delete:
* post:
* summary: 批量删除AI调用记录
* description: 批量软删除AI调用记录
* tags: [后台-AI调用记录管理]
*/
'POST /ai_call_records/batch_delete': async (ctx) => {
try {
const models = Framework.getModels();
const { ai_call_records, op } = models;
const { ids } = ctx.getBody();
if (!ids || !Array.isArray(ids) || ids.length === 0) {
return ctx.fail('记录ID列表不能为空');
}
// 批量软删除
await ai_call_records.update(
{ is_delete: 1 },
{ where: { id: { [op.in]: ids } } }
);
return ctx.success({ message: `成功删除 ${ids.length} 条记录` });
} catch (error) {
console.error('批量删除AI调用记录失败:', error);
return ctx.fail('批量删除AI调用记录失败: ' + error.message);
}
}
};

View File

@@ -0,0 +1,142 @@
const Sequelize = require('sequelize');
/**
* AI调用记录表模型
* 记录所有AI API调用的详细信息和Token使用情况
*/
module.exports = (db) => {
const ai_call_records = db.define("ai_call_records", {
user_id: {
comment: '用户ID如果是用户触发的调用',
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: null
},
sn_code: {
comment: '设备序列号',
type: Sequelize.STRING(50),
allowNull: true,
defaultValue: null
},
service_type: {
comment: '服务类型chat, completion, embedding等',
type: Sequelize.STRING(50),
allowNull: false,
defaultValue: ''
},
model_name: {
comment: 'AI模型名称gpt-4, gpt-3.5-turbo等',
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: ''
},
prompt_tokens: {
comment: '输入Token数量',
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
},
completion_tokens: {
comment: '输出Token数量',
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
},
total_tokens: {
comment: '总Token数量',
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
},
request_content: {
comment: '请求内容用户输入的prompt',
type: Sequelize.TEXT,
allowNull: true,
defaultValue: null
},
response_content: {
comment: '响应内容AI返回的结果',
type: Sequelize.TEXT,
allowNull: true,
defaultValue: null
},
cost_amount: {
comment: '本次调用费用(元)',
type: Sequelize.DECIMAL(10, 4),
allowNull: true,
defaultValue: null
},
status: {
comment: '调用状态success=成功, failed=失败, timeout=超时)',
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: 'success'
},
error_message: {
comment: '错误信息(如果调用失败)',
type: Sequelize.TEXT,
allowNull: true,
defaultValue: null
},
response_time: {
comment: '响应时间(毫秒)',
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: null
},
api_provider: {
comment: 'API提供商openai, azure, anthropic等',
type: Sequelize.STRING(50),
allowNull: false,
defaultValue: 'openai'
},
business_type: {
comment: '业务类型job_filter, chat, resume_optimization等',
type: Sequelize.STRING(50),
allowNull: true,
defaultValue: null
},
reference_id: {
comment: '关联业务ID如job_posting_id, chat_record_id等',
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: null
},
}, {
timestamps: false,
indexes: [
{
unique: false,
fields: ['user_id']
},
{
unique: false,
fields: ['sn_code']
},
{
unique: false,
fields: ['service_type']
},
{
unique: false,
fields: ['status']
},
{
unique: false,
fields: ['create_time']
},
{
unique: false,
fields: ['business_type']
},
{
unique: false,
fields: ['reference_id']
}
]
});
// ai_call_records.sync({ force: true });
return ai_call_records;
}

View File

@@ -0,0 +1,174 @@
/**
* AI调用记录工具类
* 用于记录每次AI API调用的详细信息
*/
const Framework = require("../../framework/node-core-framework.js");
class AiCallRecorder {
/**
* 记录AI调用
* @param {Object} params - 调用参数
* @param {Number} params.user_id - 用户ID
* @param {String} params.sn_code - 设备序列号
* @param {String} params.service_type - 服务类型
* @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 - 状态
* @param {String} params.error_message - 错误信息
* @param {Number} params.response_time - 响应时间(毫秒)
* @param {String} params.api_provider - API提供商
* @param {String} params.business_type - 业务类型
* @param {Number} 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.error('AI调用记录模型未找到');
return null;
}
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 || 'openai',
business_type: params.business_type || null,
reference_id: params.reference_id || null,
is_delete: 0,
create_time: new Date()
});
console.log(`AI调用已记录 - ID: ${record.id}, Model: ${params.model_name}, Tokens: ${params.total_tokens}`);
return record;
} catch (error) {
console.error('记录AI调用失败:', error);
return null;
}
}
/**
* 统计用户Token使用量
* @param {Number} user_id - 用户ID
* @param {String} startDate - 开始日期 (可选)
* @param {String} endDate - 结束日期 (可选)
* @returns {Promise<Object>} 统计结果
*/
static async getUserTokenStats(user_id, startDate = null, endDate = null) {
try {
const models = Framework.getModels();
const { ai_call_records, op } = models;
const where = {
user_id,
is_delete: 0,
status: 'success'
};
if (startDate && endDate) {
where.create_time = {
[op.between]: [new Date(startDate), new Date(endDate)]
};
} else if (startDate) {
where.create_time = {
[op.gte]: new Date(startDate)
};
} else if (endDate) {
where.create_time = {
[op.lte]: new Date(endDate)
};
}
const records = await ai_call_records.findAll({ where });
const stats = {
total_calls: records.length,
total_prompt_tokens: 0,
total_completion_tokens: 0,
total_tokens: 0,
total_cost: 0
};
records.forEach(record => {
stats.total_prompt_tokens += record.prompt_tokens || 0;
stats.total_completion_tokens += record.completion_tokens || 0;
stats.total_tokens += record.total_tokens || 0;
stats.total_cost += parseFloat(record.cost_amount || 0);
});
return stats;
} catch (error) {
console.error('统计Token使用量失败:', error);
return null;
}
}
/**
* 统计设备Token使用量
* @param {String} sn_code - 设备序列号
* @param {String} startDate - 开始日期 (可选)
* @param {String} endDate - 结束日期 (可选)
* @returns {Promise<Object>} 统计结果
*/
static async getDeviceTokenStats(sn_code, startDate = null, endDate = null) {
try {
const models = Framework.getModels();
const { ai_call_records, op } = models;
const where = {
sn_code,
is_delete: 0,
status: 'success'
};
if (startDate && endDate) {
where.create_time = {
[op.between]: [new Date(startDate), new Date(endDate)]
};
}
const records = await ai_call_records.findAll({ where });
const stats = {
total_calls: records.length,
total_prompt_tokens: 0,
total_completion_tokens: 0,
total_tokens: 0,
total_cost: 0
};
records.forEach(record => {
stats.total_prompt_tokens += record.prompt_tokens || 0;
stats.total_completion_tokens += record.completion_tokens || 0;
stats.total_tokens += record.total_tokens || 0;
stats.total_cost += parseFloat(record.cost_amount || 0);
});
return stats;
} catch (error) {
console.error('统计设备Token使用量失败:', error);
return null;
}
}
}
module.exports = AiCallRecorder;

View File

@@ -4,6 +4,7 @@
*/
const axios = require('axios');
const AiCallRecorder = require('./ai_call_recorder.js');
class AIService {
constructor(config = {}) {
@@ -30,6 +31,9 @@ class AIService {
* @returns {Promise<String>} AI响应内容
*/
async chat(messages, options = {}) {
const startTime = Date.now();
const requestContent = JSON.stringify(messages);
try {
const response = await this.client.post('/v1/chat/completions', {
model: this.model,
@@ -39,19 +43,90 @@ class AIService {
...options
});
return response.data.choices[0].message.content;
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 || 'chat',
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: 'deepseek',
business_type: options.business_type,
reference_id: options.reference_id
}).catch(err => {
console.warn('记录AI调用失败不影响主流程:', err.message);
});
return responseContent;
} catch (error) {
const responseTime = Date.now() - startTime;
// 记录失败的调用
this.recordAiCall({
user_id: options.user_id,
sn_code: options.sn_code,
service_type: options.service_type || 'chat',
model_name: this.model,
request_content: requestContent,
status: 'failed',
error_message: error.message,
response_time: responseTime,
api_provider: 'deepseek',
business_type: options.business_type,
reference_id: options.reference_id
}).catch(err => {
console.warn('记录失败调用失败:', err.message);
});
console.warn('AI服务调用失败:', error.message);
throw new Error(`AI服务调用失败: ${error.message}`);
}
}
/**
* 记录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) {
// DeepSeek 价格(元/1000 tokens
// 可以根据实际API定价调整
const pricePerThousand = 0.001; // 示例价格
return (totalTokens / 1000) * pricePerThousand;
}
/**
* 分析简历竞争力
* @param {Object} resumeData - 简历数据
* @param {Object} context - 上下文信息user_id, sn_code等
* @returns {Promise<Object>} 分析结果
*/
async analyzeResume(resumeData) {
async analyzeResume(resumeData, context = {}) {
const prompt = `请分析以下简历的竞争力,并提供详细评估:
简历信息:
@@ -91,7 +166,14 @@ class AIService {
];
try {
const response = await this.chat(messages, { temperature: 0.3 });
const response = await this.chat(messages, {
temperature: 0.3,
user_id: context.user_id,
sn_code: context.sn_code,
service_type: 'completion',
business_type: 'resume_analysis',
reference_id: resumeData.id || resumeData.resumeId
});
// 提取JSON部分
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
@@ -120,9 +202,10 @@ class AIService {
* 岗位匹配度评估
* @param {Object} jobData - 岗位数据
* @param {Object} resumeData - 简历数据
* @param {Object} context - 上下文信息user_id, sn_code等
* @returns {Promise<Object>} 匹配结果
*/
async matchJobWithResume(jobData, resumeData) {
async matchJobWithResume(jobData, resumeData, context = {}) {
const prompt = `请评估以下岗位与简历的匹配度:
【岗位信息】
@@ -169,7 +252,14 @@ class AIService {
];
try {
const response = await this.chat(messages, { temperature: 0.3 });
const response = await this.chat(messages, {
temperature: 0.3,
user_id: context.user_id,
sn_code: context.sn_code,
service_type: 'completion',
business_type: 'job_matching',
reference_id: jobData.id || jobData.jobId
});
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
@@ -197,9 +287,10 @@ class AIService {
* 批量评估岗位(用于智能筛选)
* @param {Array} jobs - 岗位列表
* @param {Object} resumeData - 简历数据
* @param {Object} context - 上下文信息user_id, sn_code等
* @returns {Promise<Array>} 评估结果列表
*/
async batchMatchJobs(jobs, resumeData) {
async batchMatchJobs(jobs, resumeData, context = {}) {
const results = [];
// 限制并发数量避免API限流
@@ -207,7 +298,7 @@ class AIService {
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 => {
this.matchJobWithResume(job, resumeData, context).catch(err => {
console.warn(`岗位${job.jobId}匹配失败:`, err.message);
return {
jobId: job.jobId,
@@ -231,11 +322,11 @@ class AIService {
/**
* 生成聊天内容
* @param {Object} context - 聊天上下文
* @param {Object} context - 聊天上下文包含jobInfo, resumeInfo, chatType, user_id, sn_code等
* @returns {Promise<String>} 生成的聊天内容
*/
async generateChatContent(context) {
const { jobInfo, resumeInfo, chatType = 'greeting', previousMessages = [] } = context;
const { jobInfo, resumeInfo, chatType = 'greeting', previousMessages = [], user_id, sn_code } = context;
let prompt = '';
@@ -279,7 +370,15 @@ class AIService {
];
try {
const response = await this.chat(messages, { temperature: 0.8, max_tokens: 200 });
const response = await this.chat(messages, {
temperature: 0.8,
max_tokens: 200,
user_id,
sn_code,
service_type: 'chat',
business_type: 'chat_generation',
reference_id: jobInfo?.jobId || jobInfo?.id
});
return response.trim();
} catch (error) {
console.warn('生成聊天内容失败:', error);
@@ -300,9 +399,10 @@ class AIService {
/**
* 判断是否为面试邀约
* @param {String} message - HR消息内容
* @param {Object} context - 上下文信息user_id, sn_code等
* @returns {Promise<Object>} 判断结果
*/
async detectInterviewInvitation(message) {
async detectInterviewInvitation(message, context = {}) {
const prompt = `判断以下HR消息是否为面试邀约并提取关键信息
消息内容: "${message}"
@@ -323,7 +423,14 @@ class AIService {
];
try {
const response = await this.chat(messages, { temperature: 0.1 });
const response = await this.chat(messages, {
temperature: 0.1,
user_id: context.user_id,
sn_code: context.sn_code,
service_type: 'completion',
business_type: 'interview_detection',
reference_id: context.conversation_id || context.job_id
});
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
@@ -349,9 +456,10 @@ class AIService {
/**
* 分析HR反馈情感
* @param {String} message - HR消息内容
* @param {Object} context - 上下文信息user_id, sn_code等
* @returns {Promise<Object>} 情感分析结果
*/
async analyzeSentiment(message) {
async analyzeSentiment(message, context = {}) {
const prompt = `分析以下HR消息的情感倾向
消息: "${message}"
@@ -370,7 +478,14 @@ class AIService {
];
try {
const response = await this.chat(messages, { temperature: 0.1 });
const response = await this.chat(messages, {
temperature: 0.1,
user_id: context.user_id,
sn_code: context.sn_code,
service_type: 'completion',
business_type: 'sentiment_analysis',
reference_id: context.conversation_id || context.job_id
});
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);