Files
autoAiWorkSys/api/middleware/job/managers/chatManager.js
张成 1a011bcc01 1
2026-02-27 17:33:39 +08:00

344 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const ai_service = require('../../../services/ai_service');
/**
* 聊天管理模块
* 负责沟通列表、沟通详情、发送消息等与设备端的 MQTT 指令对接
*/
class ChatManager {
/**
* 解析沟通列表返回值,统一为 { friendList, foldText, ... }
* 设备端可能返回 code:0 + zpData 或 code:200 + data
* @private
*/
_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 {
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>} { friendList, foldText, filterEncryptIdList, filterBossIdList }
*/
async get_chat_list(sn_code, mqttClient, params = {}) {
const { platform = 'boss', pageCount = 3 } = params;
console.log(`[聊天管理] 开始获取设备 ${sn_code} 的聊天列表`);
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: 'get_chat_list',
data: { pageCount }
});
// 沟通列表接口成功为 code: 0 或 code: 200
const ok = response && (response.code === 0 || response.code === 200);
if (!ok) {
console.error(`[聊天管理] 获取聊天列表失败:`, response);
throw new Error(response?.message || '获取聊天列表失败');
}
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回复完成'
};
}
}
module.exports = new ChatManager();