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; } /** * 解析 get_chat_detail 设备端返回格式 * 格式: { type, code, message, data: { success, apiData: { response: { zpData } }, getBossData: { response: { zpData } } } } * apiData.response.zpData = 消息列表 hasMore/messages/type/minMsgId * getBossData.response.zpData = 会话 data+job * @private */ _parse_chat_detail_response(response) { if (!response) return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] }; const d = response.data; const api_data = d && d.apiData; const get_boss_data = d && d.getBossData; const msg_zp = api_data && api_data.response && api_data.response.zpData; const boss_zp = get_boss_data && get_boss_data.response && get_boss_data.response.zpData; if (msg_zp && Array.isArray(msg_zp.messages)) { return { variant: 'messages', hasMore: !!msg_zp.hasMore, messages: msg_zp.messages, type: msg_zp.type, minMsgId: msg_zp.minMsgId, data: (boss_zp && boss_zp.data) || null, job: (boss_zp && boss_zp.job) || null }; } if (boss_zp && (boss_zp.data != null || boss_zp.job != null)) { return { variant: 'session', data: boss_zp.data || null, job: boss_zp.job || null, hasMore: false, messages: [], type: null, minMsgId: null }; } return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] }; } /** * 解析详情,统一返回 { variant, hasMore, minMsgId, messages, data, job } * 入参可为设备端完整返回(data.apiData/data.getBossData)或已解析对象(直接返回) */ parseDetailResponse(apiResponse) { if (apiResponse && (apiResponse.variant === 'messages' || apiResponse.variant === 'session' || apiResponse.variant === 'unknown')) { return apiResponse; } return this._parse_chat_detail_response(apiResponse); } /** * 获取沟通详情(会话信息或聊天消息列表) * 返回值: { 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 === 200 || response.code === 0); 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 parsed = await this.get_chat_detail(sn_code, mqttClient, { platform, ...detailParams }); const decision = await this.getReplyContentFromDetail(parsed); 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();