const ai_service_module = require('../../../services/ai_service'); const ai_service = ai_service_module.getInstance(); /** * 聊天管理模块 * 负责沟通列表、沟通详情、发送消息等与设备端的 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} { 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} */ 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} 发送结果 */ 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} { 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} { 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();