1
This commit is contained in:
@@ -1,423 +1,344 @@
|
||||
const aiServiceModule = require('../../../services/ai_service');
|
||||
const logs = require('../../logProxy');
|
||||
|
||||
// 实例化AI服务
|
||||
const aiService = aiServiceModule.getInstance();
|
||||
const ai_service_module = require('../../../services/ai_service');
|
||||
const ai_service = ai_service_module.getInstance();
|
||||
|
||||
/**
|
||||
* 智能聊天管理模块
|
||||
* 负责聊天内容生成、发送策略和效果监控
|
||||
* 聊天管理模块
|
||||
* 负责沟通列表、沟通详情、发送消息等与设备端的 MQTT 指令对接
|
||||
*/
|
||||
class ChatManager {
|
||||
constructor() {
|
||||
this.chatHistory = new Map(); // 聊天历史记录
|
||||
this.chatStrategies = new Map(); // 聊天策略配置
|
||||
this.effectStats = new Map(); // 聊天效果统计
|
||||
this.initDefaultStrategies();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认聊天策略
|
||||
* 解析沟通列表返回值,统一为 { friendList, foldText, ... }
|
||||
* 设备端可能返回 code:0 + zpData 或 code:200 + data
|
||||
* @private
|
||||
*/
|
||||
initDefaultStrategies() {
|
||||
// 初次打招呼策略
|
||||
this.chatStrategies.set('greeting', {
|
||||
name: '初次打招呼',
|
||||
description: '向HR发送初次打招呼消息',
|
||||
template: 'greeting',
|
||||
timing: 'immediate',
|
||||
retryCount: 1,
|
||||
retryInterval: 300000 // 5分钟
|
||||
});
|
||||
|
||||
// 面试邀约策略
|
||||
this.chatStrategies.set('interview', {
|
||||
name: '面试邀约',
|
||||
description: '发送面试邀约消息',
|
||||
template: 'interview',
|
||||
timing: 'after_greeting',
|
||||
retryCount: 2,
|
||||
retryInterval: 600000 // 10分钟
|
||||
});
|
||||
|
||||
// 跟进沟通策略
|
||||
this.chatStrategies.set('followup', {
|
||||
name: '跟进沟通',
|
||||
description: '跟进之前的沟通',
|
||||
template: 'followup',
|
||||
timing: 'after_interview',
|
||||
retryCount: 1,
|
||||
retryInterval: 86400000 // 24小时
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成聊天内容
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @param {string} chatType - 聊天类型
|
||||
* @param {object} context - 聊天上下文
|
||||
* @returns {Promise<object>} 聊天内容
|
||||
*/
|
||||
async generateChatContent(sn_code, jobInfo, resumeInfo, chatType = 'greeting', context = {}) {
|
||||
console.log(`[聊天管理] 开始生成设备 ${sn_code} 的聊天内容,类型: ${chatType}`);
|
||||
|
||||
// 获取聊天策略
|
||||
const strategy = this.chatStrategies.get(chatType);
|
||||
if (!strategy) {
|
||||
throw new Error(`未找到聊天类型 ${chatType} 的策略配置`);
|
||||
}
|
||||
|
||||
// 二期规划:AI 生成聊天内容暂时禁用,使用默认模板
|
||||
// const chatContent = await aiService.generateChatContent(jobInfo, resumeInfo, chatType);
|
||||
// if (!chatContent.success) {
|
||||
// console.error(`[聊天管理] AI生成聊天内容失败:`, chatContent.error);
|
||||
// throw new Error(chatContent.error);
|
||||
// }
|
||||
|
||||
console.log(`[聊天管理] AI生成已禁用(二期规划),使用默认聊天模板`);
|
||||
const chatContent = this.generateDefaultChatContent(jobInfo, resumeInfo, chatType);
|
||||
|
||||
const result = {
|
||||
sn_code: sn_code,
|
||||
jobInfo: jobInfo,
|
||||
chatType: chatType,
|
||||
strategy: strategy,
|
||||
content: chatContent.content,
|
||||
context: context,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 记录聊天历史
|
||||
this.recordChatHistory(sn_code, result);
|
||||
|
||||
console.log(`[聊天管理] 聊天内容生成成功:`, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} mqttClient - MQTT客户端
|
||||
* @param {object} chatData - 聊天数据
|
||||
* @returns {Promise<object>} 发送结果
|
||||
*/
|
||||
async sendChatMessage(sn_code, mqttClient, chatData) {
|
||||
console.log(`[聊天管理] 开始发送聊天消息到设备 ${sn_code}`);
|
||||
|
||||
// 构建发送指令
|
||||
const sendData = {
|
||||
platform: 'boss',
|
||||
action: 'send_chat_message',
|
||||
data: {
|
||||
jobId: chatData.jobInfo.jobId,
|
||||
companyId: chatData.jobInfo.companyId,
|
||||
message: chatData.content,
|
||||
chatType: chatData.chatType
|
||||
}
|
||||
};
|
||||
|
||||
// 发送MQTT指令
|
||||
const response = await mqttClient.publishAndWait(sn_code, sendData);
|
||||
|
||||
if (!response || response.code !== 200) {
|
||||
// 更新聊天状态
|
||||
this.updateChatStatus(sn_code, chatData, 'failed', response);
|
||||
|
||||
console.error(`[聊天管理] 聊天消息发送失败:`, response);
|
||||
throw new Error(response?.message || '聊天消息发送失败');
|
||||
}
|
||||
|
||||
// 更新聊天状态
|
||||
this.updateChatStatus(sn_code, chatData, 'sent', response);
|
||||
|
||||
// 记录效果统计
|
||||
this.recordChatEffect(sn_code, chatData, 'sent');
|
||||
|
||||
console.log(`[聊天管理] 聊天消息发送成功:`, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成面试邀约
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} chatHistory - 聊天历史
|
||||
* @returns {Promise<object>} 面试邀约内容
|
||||
*/
|
||||
async generateInterviewInvitation(sn_code, jobInfo, chatHistory) {
|
||||
console.log(`[聊天管理] 开始生成设备 ${sn_code} 的面试邀约`);
|
||||
console.log(`[聊天管理] AI生成已禁用(二期规划),使用默认模板`);
|
||||
|
||||
// 二期规划:AI 生成面试邀约暂时禁用,使用默认模板
|
||||
// const invitation = await aiService.generateInterviewInvitation(jobInfo, chatHistory);
|
||||
// if (!invitation.success) {
|
||||
// console.error(`[聊天管理] AI生成面试邀约失败:`, invitation.error);
|
||||
// throw new Error(invitation.error);
|
||||
// }
|
||||
|
||||
const invitation = this.generateDefaultInterviewInvitation(jobInfo);
|
||||
|
||||
const result = {
|
||||
sn_code: sn_code,
|
||||
jobInfo: jobInfo,
|
||||
chatType: 'interview',
|
||||
content: invitation.content,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 记录聊天历史
|
||||
this.recordChatHistory(sn_code, result);
|
||||
|
||||
console.log(`[聊天管理] 面试邀约生成成功:`, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录聊天历史
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} chatData - 聊天数据
|
||||
*/
|
||||
recordChatHistory(sn_code, chatData) {
|
||||
if (!this.chatHistory.has(sn_code)) {
|
||||
this.chatHistory.set(sn_code, []);
|
||||
}
|
||||
|
||||
const history = this.chatHistory.get(sn_code);
|
||||
history.push({
|
||||
...chatData,
|
||||
id: Date.now() + Math.random(),
|
||||
status: 'generated'
|
||||
});
|
||||
|
||||
// 限制历史记录数量
|
||||
if (history.length > 100) {
|
||||
history.splice(0, history.length - 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新聊天状态
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} chatData - 聊天数据
|
||||
* @param {string} status - 新状态
|
||||
* @param {object} response - 响应数据
|
||||
*/
|
||||
updateChatStatus(sn_code, chatData, status, response = {}) {
|
||||
if (!this.chatHistory.has(sn_code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const history = this.chatHistory.get(sn_code);
|
||||
const chatRecord = history.find(record =>
|
||||
record.timestamp === chatData.timestamp &&
|
||||
record.chatType === chatData.chatType
|
||||
);
|
||||
|
||||
if (chatRecord) {
|
||||
chatRecord.status = status;
|
||||
chatRecord.response = response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录聊天效果
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} chatData - 聊天数据
|
||||
* @param {string} action - 动作类型
|
||||
*/
|
||||
recordChatEffect(sn_code, chatData, action) {
|
||||
if (!this.effectStats.has(sn_code)) {
|
||||
this.effectStats.set(sn_code, {
|
||||
totalSent: 0,
|
||||
totalReplied: 0,
|
||||
totalInterview: 0,
|
||||
replyRate: 0,
|
||||
interviewRate: 0,
|
||||
lastUpdate: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
const stats = this.effectStats.get(sn_code);
|
||||
|
||||
if (action === 'sent') {
|
||||
stats.totalSent++;
|
||||
} else if (action === 'replied') {
|
||||
stats.totalReplied++;
|
||||
} else if (action === 'interview') {
|
||||
stats.totalInterview++;
|
||||
}
|
||||
|
||||
// 计算比率
|
||||
if (stats.totalSent > 0) {
|
||||
stats.replyRate = (stats.totalReplied / stats.totalSent * 100).toFixed(2);
|
||||
stats.interviewRate = (stats.totalInterview / stats.totalSent * 100).toFixed(2);
|
||||
}
|
||||
|
||||
stats.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天历史
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} filters - 过滤条件
|
||||
* @returns {Array} 聊天历史
|
||||
*/
|
||||
getChatHistory(sn_code, filters = {}) {
|
||||
if (!this.chatHistory.has(sn_code)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let history = this.chatHistory.get(sn_code);
|
||||
|
||||
// 应用过滤条件
|
||||
if (filters.chatType) {
|
||||
history = history.filter(record => record.chatType === filters.chatType);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
history = history.filter(record => record.status === filters.status);
|
||||
}
|
||||
|
||||
if (filters.startTime) {
|
||||
history = history.filter(record => record.timestamp >= filters.startTime);
|
||||
}
|
||||
|
||||
if (filters.endTime) {
|
||||
history = history.filter(record => record.timestamp <= filters.endTime);
|
||||
}
|
||||
|
||||
// 按时间倒序排列
|
||||
return history.sort((a, b) => b.timestamp - a.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天效果统计
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @returns {object} 效果统计
|
||||
*/
|
||||
getChatEffectStats(sn_code) {
|
||||
if (!this.effectStats.has(sn_code)) {
|
||||
return {
|
||||
totalSent: 0,
|
||||
totalReplied: 0,
|
||||
totalInterview: 0,
|
||||
replyRate: 0,
|
||||
interviewRate: 0,
|
||||
lastUpdate: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
return this.effectStats.get(sn_code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置聊天策略
|
||||
* @param {string} chatType - 聊天类型
|
||||
* @param {object} strategy - 策略配置
|
||||
*/
|
||||
setChatStrategy(chatType, strategy) {
|
||||
this.chatStrategies.set(chatType, {
|
||||
...this.chatStrategies.get(chatType),
|
||||
...strategy
|
||||
});
|
||||
|
||||
console.log(`[聊天管理] 更新聊天策略 ${chatType}:`, strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期数据
|
||||
*/
|
||||
cleanup() {
|
||||
const now = Date.now();
|
||||
const expireTime = 30 * 24 * 3600000; // 30天
|
||||
|
||||
// 清理过期的聊天历史
|
||||
for (const [sn_code, history] of this.chatHistory.entries()) {
|
||||
const filteredHistory = history.filter(record =>
|
||||
now - record.timestamp < expireTime
|
||||
);
|
||||
|
||||
if (filteredHistory.length === 0) {
|
||||
this.chatHistory.delete(sn_code);
|
||||
} else {
|
||||
this.chatHistory.set(sn_code, filteredHistory);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期的效果统计
|
||||
for (const [sn_code, stats] of this.effectStats.entries()) {
|
||||
if (now - stats.lastUpdate > expireTime) {
|
||||
this.effectStats.delete(sn_code);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[聊天管理] 数据清理完成`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认聊天内容(替代 AI 生成)
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @param {string} chatType - 聊天类型
|
||||
* @returns {object} 聊天内容
|
||||
*/
|
||||
generateDefaultChatContent(jobInfo, resumeInfo, chatType) {
|
||||
const templates = {
|
||||
greeting: '您好,我对这个岗位很感兴趣,希望能进一步了解。',
|
||||
interview: '感谢您的回复,我很期待与您进一步沟通。',
|
||||
followup: '您好,想了解一下这个岗位的最新进展。'
|
||||
};
|
||||
|
||||
const content = templates[chatType] || templates.greeting;
|
||||
|
||||
_parse_chat_list_response(response) {
|
||||
if (!response) return null;
|
||||
const raw = response.zpData != null ? response.zpData : response.data;
|
||||
if (!raw) return { friendList: [], foldText: '', filterEncryptIdList: [], filterBossIdList: [] };
|
||||
return {
|
||||
success: true,
|
||||
content: content,
|
||||
chatType: chatType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认面试邀约(替代 AI 生成)
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @returns {object} 面试邀约内容
|
||||
*/
|
||||
generateDefaultInterviewInvitation(jobInfo) {
|
||||
return {
|
||||
success: true,
|
||||
content: '感谢您的邀请,我很期待与您面谈。请问方便的时间是什么时候?',
|
||||
jobTitle: jobInfo.jobTitle || '该岗位',
|
||||
companyName: jobInfo.companyName || '贵公司'
|
||||
friendList: Array.isArray(raw.friendList) ? raw.friendList : [],
|
||||
foldText: raw.foldText || '',
|
||||
filterEncryptIdList: Array.isArray(raw.filterEncryptIdList) ? raw.filterEncryptIdList : [],
|
||||
filterBossIdList: Array.isArray(raw.filterBossIdList) ? raw.filterBossIdList : []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天列表
|
||||
* 返回值结构: { friendList, foldText, filterEncryptIdList, filterBossIdList }
|
||||
* friendList 每项: friendId, encryptFriendId, name, updateTime, brandName, jobName, jobCity, positionName, bossTitle 等
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} mqttClient - MQTT客户端
|
||||
* @param {object} params - 参数
|
||||
* @returns {Promise<object>} 聊天列表
|
||||
* @returns {Promise<object>} { friendList, foldText, filterEncryptIdList, filterBossIdList }
|
||||
*/
|
||||
async get_chat_list(sn_code, mqttClient, params = {}) {
|
||||
const { platform = 'boss', pageCount = 3 } = params;
|
||||
console.log(`[聊天管理] 开始获取设备 ${sn_code} 的聊天列表`);
|
||||
|
||||
// 通过MQTT指令获取聊天列表
|
||||
const response = await mqttClient.publishAndWait(sn_code, {
|
||||
platform,
|
||||
action: "get_chat_list",
|
||||
action: 'get_chat_list',
|
||||
data: { pageCount }
|
||||
});
|
||||
|
||||
if (!response || response.code !== 200) {
|
||||
// 沟通列表接口成功为 code: 0 或 code: 200
|
||||
const ok = response && (response.code === 0 || response.code === 200);
|
||||
if (!ok) {
|
||||
console.error(`[聊天管理] 获取聊天列表失败:`, response);
|
||||
throw new Error('获取聊天列表失败');
|
||||
throw new Error(response?.message || '获取聊天列表失败');
|
||||
}
|
||||
|
||||
console.log(`[聊天管理] 成功获取聊天列表`);
|
||||
return response.data;
|
||||
const parsed = this._parse_chat_list_response(response);
|
||||
console.log(`[聊天管理] 成功获取聊天列表,共 ${parsed.friendList.length} 个联系人`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析沟通详情返回值(两种形态二选一)
|
||||
* 形态1 - 会话/职位信息: zpData.data + zpData.job
|
||||
* 形态2 - 聊天消息列表: zpData.hasMore + zpData.messages
|
||||
* @private
|
||||
*/
|
||||
_parse_chat_detail_response(response) {
|
||||
if (!response) return null;
|
||||
const raw = response.zpData != null ? response.zpData : response.data;
|
||||
if (!raw) return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] };
|
||||
|
||||
// 形态2: 消息列表(有 messages 数组)
|
||||
if (Array.isArray(raw.messages)) {
|
||||
return {
|
||||
variant: 'messages',
|
||||
hasMore: !!raw.hasMore,
|
||||
messages: raw.messages,
|
||||
type: raw.type,
|
||||
minMsgId: raw.minMsgId
|
||||
};
|
||||
}
|
||||
|
||||
// 形态1: 会话详情(data + job)
|
||||
if (raw.data != null || raw.job != null) {
|
||||
return {
|
||||
variant: 'session',
|
||||
data: raw.data || null,
|
||||
job: raw.job || null
|
||||
};
|
||||
}
|
||||
|
||||
return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取沟通详情(会话信息或聊天消息列表)
|
||||
* 返回值: { variant: 'session'|'messages', ... }
|
||||
* - session: data(boss/会话信息), job(职位信息)
|
||||
* - messages: hasMore, messages[], type, minMsgId
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} mqttClient - MQTT客户端
|
||||
* @param {object} params - 参数,如 friendId/encryptBossId/encryptJobId 等,由设备端约定
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async get_chat_detail(sn_code, mqttClient, params = {}) {
|
||||
const { platform = 'boss', ...rest } = params;
|
||||
console.log(`[聊天管理] 开始获取设备 ${sn_code} 的沟通详情`);
|
||||
|
||||
const response = await mqttClient.publishAndWait(sn_code, {
|
||||
platform,
|
||||
action: 'get_chat_detail',
|
||||
data: rest
|
||||
});
|
||||
|
||||
const ok = response && (response.code === 0 || response.code === 200);
|
||||
if (!ok) {
|
||||
console.error(`[聊天管理] 获取沟通详情失败:`, response);
|
||||
throw new Error(response?.message || '获取沟通详情失败');
|
||||
}
|
||||
|
||||
const parsed = this._parse_chat_detail_response(response);
|
||||
const logExtra = parsed.variant === 'session'
|
||||
? `会话`
|
||||
: parsed.variant === 'messages'
|
||||
? `消息 ${parsed.messages.length} 条`
|
||||
: `未知`;
|
||||
console.log(`[聊天管理] 成功获取沟通详情 (${logExtra})`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天消息(支持多条 + 文本/发简历/换电话/换微信)
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} mqttClient - MQTT客户端
|
||||
* @param {object} params - friendId(必填), messages(数组), chatType, use_real_type, platform
|
||||
* @param {string} params.friendId - 好友ID,用于打开该好友的聊天面板
|
||||
* @param {Array} params.messages - 每项为 string 或 { type: 'text'|'send_resume'|'exchange_phone'|'exchange_wechat', content?: string }
|
||||
* @param {boolean} params.use_real_type - 是否模拟真实打字,默认 false
|
||||
* @returns {Promise<object>} 发送结果
|
||||
*/
|
||||
async send_chat_message(sn_code, mqttClient, params) {
|
||||
const { friendId, messages, chatType, use_real_type = false, platform = 'boss' } = params || {};
|
||||
|
||||
if (!friendId) throw new Error('friendId 不能为空');
|
||||
if (!Array.isArray(messages) || messages.length === 0) throw new Error('messages 必须是非空数组');
|
||||
|
||||
const normalized_messages = messages.map((item) => {
|
||||
if (typeof item === 'string') return { type: 'text', content: item };
|
||||
return { type: item.type || 'text', content: item.content || '' };
|
||||
});
|
||||
|
||||
console.log(`[聊天管理] 设备 ${sn_code} 发送聊天消息,friendId=${friendId},条数=${normalized_messages.length}`);
|
||||
|
||||
const response = await mqttClient.publishAndWait(sn_code, {
|
||||
platform,
|
||||
action: 'send_chat_message',
|
||||
data: { friendId, messages: normalized_messages, chatType, use_real_type: !!use_real_type }
|
||||
});
|
||||
|
||||
if (!response || (response.code !== 0 && response.code !== 200)) {
|
||||
console.error(`[聊天管理] 聊天消息发送失败:`, response);
|
||||
throw new Error(response?.message || '聊天消息发送失败');
|
||||
}
|
||||
|
||||
console.log(`[聊天管理] 聊天消息发送成功`);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 AI 自动决定是否回复,并发送回复
|
||||
* 流程:
|
||||
* 1. 根据参数获取沟通详情(消息列表)
|
||||
* 2. 如果最后一句是 HR 说的,则调用阿里云 Qwen 生成回复文案
|
||||
* 3. 通过 send_chat_message 把回复发出去
|
||||
*
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} mqttClient - MQTT客户端
|
||||
* @param {object} params - 包含 friendId + 获取详情所需参数(如 encryptBossId/encryptJobId 等)
|
||||
* @returns {Promise<object>} { replied, reply_content?, hr_message_text?, reason? }
|
||||
*/
|
||||
async auto_reply_with_ai(sn_code, mqttClient, params = {}) {
|
||||
const { friendId, platform = 'boss', ...detailParams } = params;
|
||||
|
||||
if (!friendId) {
|
||||
throw new Error('friendId 不能为空');
|
||||
}
|
||||
|
||||
// 1. 获取沟通详情(期望拿到消息列表)
|
||||
const detail = await this.get_chat_detail(sn_code, mqttClient, {
|
||||
platform,
|
||||
...detailParams
|
||||
});
|
||||
|
||||
if (!detail || detail.variant !== 'messages' || !Array.isArray(detail.messages) || detail.messages.length === 0) {
|
||||
return { replied: false, reason: '无可用消息' };
|
||||
}
|
||||
|
||||
const messages = detail.messages;
|
||||
|
||||
// 2. 推断 HR 与 求职者 uid
|
||||
let hr_uid = null;
|
||||
let geek_uid = null;
|
||||
|
||||
for (const msg of messages) {
|
||||
const body = msg.body || {};
|
||||
const jobDesc = body.jobDesc || body.job_desc || null;
|
||||
if (jobDesc) {
|
||||
if (jobDesc.boss && jobDesc.boss.uid && !hr_uid) {
|
||||
hr_uid = jobDesc.boss.uid;
|
||||
}
|
||||
if (jobDesc.geek && jobDesc.geek.uid && !geek_uid) {
|
||||
geek_uid = jobDesc.geek.uid;
|
||||
}
|
||||
}
|
||||
if (hr_uid && geek_uid) break;
|
||||
}
|
||||
|
||||
const last = messages[messages.length - 1];
|
||||
|
||||
// 兜底:还没有 hr_uid 时,用最后一条的 from/to 做简单推断
|
||||
if ((!hr_uid || !geek_uid) && last && last.from && last.to) {
|
||||
hr_uid = hr_uid || last.from.uid;
|
||||
geek_uid = geek_uid || last.to.uid;
|
||||
}
|
||||
|
||||
if (!last || !last.from || !hr_uid || last.from.uid !== hr_uid) {
|
||||
// 最后一条不是 HR 发的,不自动回复
|
||||
return { replied: false, reason: '最后一条不是HR消息' };
|
||||
}
|
||||
|
||||
// 取 HR 文本内容(普通文本优先)
|
||||
const body = last.body || {};
|
||||
const hr_message_text =
|
||||
(typeof body.text === 'string' && body.text) ||
|
||||
(typeof last.pushText === 'string' && last.pushText) ||
|
||||
'';
|
||||
|
||||
if (!hr_message_text || !hr_message_text.trim()) {
|
||||
return { replied: false, reason: 'HR消息没有可用文本' };
|
||||
}
|
||||
|
||||
// 3. 调用阿里云 Qwen 生成回复文案(已在 config 中切换为 qwen-plus)
|
||||
const jobInfo = detail.job || {};
|
||||
|
||||
const reply_content = await ai_service.generateChatContent({
|
||||
jobInfo,
|
||||
resumeInfo: null,
|
||||
chatType: 'reply',
|
||||
hrMessage: hr_message_text,
|
||||
previousMessages: [] // 如需上下文,这里可以把 detail.messages 映射进去
|
||||
});
|
||||
|
||||
if (!reply_content || !reply_content.trim()) {
|
||||
return { replied: false, reason: 'AI 未生成有效回复' };
|
||||
}
|
||||
|
||||
// 4. 通过统一的 send_chat_message 下发回复
|
||||
await this.send_chat_message(sn_code, mqttClient, {
|
||||
friendId,
|
||||
messages: [{ type: 'text', content: reply_content }],
|
||||
chatType: 'reply',
|
||||
platform
|
||||
});
|
||||
|
||||
return {
|
||||
replied: true,
|
||||
reply_content,
|
||||
hr_message_text
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动获取沟通列表 + 按会话自动 AI 回复
|
||||
* 1. 调用 get_chat_list 获取会话列表
|
||||
* 2. 对每个会话按 friendId 调用 auto_reply_with_ai(内部会先获取详情,再决定是否回复)
|
||||
*
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} mqttClient - MQTT客户端
|
||||
* @param {object} params - { platform?, pageCount? }
|
||||
* @returns {Promise<object>} { success, total_contacts, replied_count, details: [...] }
|
||||
*/
|
||||
async auto_chat_ai(sn_code, mqttClient, params = {}) {
|
||||
const { platform = 'boss', pageCount = 3 } = params;
|
||||
|
||||
// 1. 获取沟通列表
|
||||
const listResult = await this.get_chat_list(sn_code, mqttClient, {
|
||||
platform,
|
||||
pageCount
|
||||
});
|
||||
|
||||
const friendList = Array.isArray(listResult.friendList) ? listResult.friendList : [];
|
||||
|
||||
if (friendList.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
total_contacts: 0,
|
||||
replied_count: 0,
|
||||
details: [],
|
||||
message: '没有可沟通的会话'
|
||||
};
|
||||
}
|
||||
|
||||
let replied_count = 0;
|
||||
const details = [];
|
||||
|
||||
// 2. 逐个会话顺序处理,避免并发下发指令
|
||||
for (const friend of friendList) {
|
||||
const friendId = friend.friendId;
|
||||
if (!friendId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await this.auto_reply_with_ai(sn_code, mqttClient, {
|
||||
platform,
|
||||
friendId
|
||||
});
|
||||
|
||||
if (r.replied) {
|
||||
replied_count++;
|
||||
}
|
||||
|
||||
details.push({
|
||||
friendId,
|
||||
replied: !!r.replied,
|
||||
reason: r.reason || null,
|
||||
reply_content: r.reply_content || null
|
||||
});
|
||||
} catch (error) {
|
||||
details.push({
|
||||
friendId,
|
||||
replied: false,
|
||||
reason: error.message || '自动回复失败',
|
||||
reply_content: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
total_contacts: friendList.length,
|
||||
replied_count,
|
||||
details,
|
||||
message: '自动获取列表并尝试AI回复完成'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user