const ai_service = require('../../../services/ai_service'); /** * 聊天管理模块 * 负责沟通列表、沟通详情、发送消息等与设备端的 MQTT 指令对接 */ class ChatManager { /** * 解析沟通列表返回值,统一为 { friendList, foldText, ... } * 只支持新的结构: * response.data = { success, apiData: [ { response: { code, zpData:{...} } } ] } * @private */ _parse_chat_list_response(response) { const outerData = response && response.data; if (!outerData || !Array.isArray(outerData.apiData) || outerData.apiData.length === 0) { return { friendList: [], foldText: '', filterEncryptIdList: [], filterBossIdList: [] }; } const firstApi = outerData.apiData[0] || {}; const innerResp = firstApi.response || firstApi.data || null; const raw = innerResp && (innerResp.zpData != null ? innerResp.zpData : innerResp.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 } }); // 只认新结构:data.success === true const ok = !!response && response.data && response.data.success === true; 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; } /** * 根据沟通详情(get_chat_detail 的解析结果)判断是否需回复,并用 AI 生成回复文案 * 供任务层在「获取详情」指令执行后调用,不包含发送消息(由任务层再下发 send_chat_message 指令) * * @param {object} detail - 沟通详情,含 variant、messages、job 等 * @returns {Promise} { replied: true, reply_content, hr_message_text } | { replied: false, reason } */ async getReplyContentFromDetail(detail) { if (!detail || detail.variant !== 'messages' || !Array.isArray(detail.messages) || detail.messages.length === 0) { return { replied: false, reason: '无可用消息' }; } const messages = detail.messages; // 推断 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 未生成有效回复' }; } return { replied: true, reply_content, hr_message_text }; } /** * 使用 AI 自动决定是否回复,并发送回复(内部会先获取详情,再调用 getReplyContentFromDetail,再发送) * 单条指令场景用;任务 auto_chat 已改为下发 get_chat_list / get_chat_detail / send_chat_message 多条指令。 * * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 包含 friendId + 获取详情所需参数 * @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 不能为空'); const detail = await this.get_chat_detail(sn_code, mqttClient, { platform, ...detailParams }); const decision = await this.getReplyContentFromDetail(detail); if (!decision.replied) return decision; await this.send_chat_message(sn_code, mqttClient, { friendId, messages: [{ type: 'text', content: decision.reply_content }], chatType: 'reply', platform }); return { replied: true, reply_content: decision.reply_content, hr_message_text: decision.hr_message_text }; } } module.exports = new ChatManager();