Files
autoAiWorkSys/api/middleware/job/managers/chatManager.js
张成 dfd3119163 1
2026-02-28 10:38:28 +08:00

289 lines
12 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, ... }
* 只支持新的结构:
* 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<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 }
});
// 只认新结构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<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;
}
/**
* 根据沟通详情get_chat_detail 的解析结果)判断是否需回复,并用 AI 生成回复文案
* 供任务层在「获取详情」指令执行后调用,不包含发送消息(由任务层再下发 send_chat_message 指令)
*
* @param {object} detail - 沟通详情,含 variant、messages、job 等
* @returns {Promise<object>} { 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<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 不能为空');
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();