const ai_service = require('../../../services/ai_service'); const db = require('../../dbProxy'); /** * 聊天管理模块 * 负责沟通列表、沟通详情、发送消息等与设备端的 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; } /** 是否为系统/模板消息(竞争者PK、拒绝模板、系统卡片等),不参与回复判断 */ _isSystemMessage(msg) { const body = msg.body || {}; if (msg.bizType === 317 || msg.bizType === 21050003) return true; if (msg.type === 4) return true; if (body.type === 16) return true; return false; } /** 过滤出 HR 发的、非系统、可回复的消息列表(已排除自己发的) */ _filterHrReplyableMessages(messages, geek_uid) { if (!geek_uid || !Array.isArray(messages)) return []; let list = messages.filter(msg => { if (!msg.from || msg.from.uid === geek_uid) return false; if (this._isSystemMessage(msg)) return false; return true; }); return list } /** AI 回复后写入 chat_reply_intent_log,options 含 sn_code/platform/friendId/encryptFriendId 时落库 */ _saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, replied, reason) { if (!options || options.sn_code == null) return; try { const model = db.getModel('chat_reply_intent_log'); model.create({ sn_code: options.sn_code || '', platform: options.platform || 'boss', friendId: options.friendId ?? null, encrypt_friend_id: options.encryptFriendId || '', hr_message_text: hr_message_text || null, action: action || '', reply_content: reply_content || null, replied: !!replied, reason: reason || null, job_name: (jobInfo && (jobInfo.jobName || jobInfo.title)) || null, create_time: new Date() }).catch(e => console.warn('[聊天管理] 写入 chat_reply_intent_log 失败:', e.message)); } catch (e) { console.warn('[聊天管理] 写入 chat_reply_intent_log 失败:', e.message); } } /** * 根据沟通详情判断是否需回复,并用 AI 判断意图(文字回复 / 发简历)及生成内容 * @param {object} detail - 沟通详情,含 variant、messages、job 等 * @param {object} options - 可选,落库用 { sn_code, platform, friendId, encryptFriendId } * @returns {Promise} { replied, action?, reply_content?, hr_message_text?, reason? } */ async getReplyContentFromDetail(detail, options) { if (!detail || detail.variant !== 'messages' || !Array.isArray(detail.messages) || detail.messages.length === 0) { return { replied: false, reason: '无可用消息' }; } const messages = detail.messages; 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 lastRaw = messages[messages.length - 1]; if (lastRaw && lastRaw.from && lastRaw.to) { hr_uid = hr_uid || lastRaw.from.uid; geek_uid = geek_uid || lastRaw.to.uid; } const jobInfo = detail.job || {}; const hrList = this._filterHrReplyableMessages(messages, geek_uid); if (hrList.length === 0) { this._saveReplyIntentLog(options, '', jobInfo, '', '', false, '无HR可回复消息(已过滤系统与己方)'); return { replied: false, reason: '无HR可回复消息(已过滤系统与己方)' }; } const last = hrList[hrList.length - 1]; if (!last.from || last.from.uid !== hr_uid) { this._saveReplyIntentLog(options, '', jobInfo, '', '', false, '最后一条可回复消息不是HR'); return { replied: false, reason: '最后一条可回复消息不是HR' }; } const body = last.body || {}; const hr_message_text = (typeof body.text === 'string' && body.text) || (typeof last.pushText === 'string' && last.pushText) || ''; const { action, reply_content } = await ai_service.replyIntentAndContent({ jobInfo, hrMessage: hr_message_text, previousMessages: hrList.slice(-5).map(m => (m.body && m.body.text) || m.pushText || '') }); if (action === 'no_reply') { this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, false, 'HR表示暂不匹配/无需回复'); return { replied: false, reason: 'HR表示暂不匹配/无需回复' }; } const needContent = action === 'text'; if (needContent && (!reply_content || !reply_content.trim())) { this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, false, 'AI 未生成有效回复文案'); return { replied: false, reason: 'AI 未生成有效回复文案' }; } this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, true, null); return { replied: true, action: action || 'text', reply_content: 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, { sn_code, platform, friendId, encryptFriendId: detailParams.encryptFriendId || '' }); if (!decision.replied) return decision; const action = decision.action || 'text'; const content = decision.reply_content || ''; const actionMessages = { send_resume: [{ type: 'send_resume', content }], exchange_wechat: [{ type: 'exchange_wechat', content }], exchange_phone: [{ type: 'exchange_phone', content }] }; const messages = actionMessages[action] || [{ type: 'text', content }]; await this.send_chat_message(sn_code, mqttClient, { friendId, messages, chatType: 'reply', platform }); return { replied: true, reply_content: decision.reply_content, hr_message_text: decision.hr_message_text }; } } module.exports = new ChatManager();