1
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const ai_service = require('../../../services/ai_service');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
/**
|
||||
* 聊天管理模块
|
||||
@@ -63,13 +64,13 @@ class ChatManager {
|
||||
|
||||
|
||||
// 存储数据库
|
||||
|
||||
|
||||
|
||||
console.log(`[聊天管理] 成功获取聊天列表,共 ${parsed.friendList.length} 个联系人`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 解析 get_chat_detail 设备端返回格式
|
||||
* 格式: { type, code, message, data: { success, apiData: { response: { zpData } }, getBossData: { response: { zpData } } } }
|
||||
@@ -195,21 +196,63 @@ class ChatManager {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据沟通详情(get_chat_detail 的解析结果)判断是否需回复,并用 AI 生成回复文案
|
||||
* 供任务层在「获取详情」指令执行后调用,不包含发送消息(由任务层再下发 send_chat_message 指令)
|
||||
*
|
||||
* 根据沟通详情判断是否需回复,并用 AI 判断意图(文字回复 / 发简历)及生成内容
|
||||
* @param {object} detail - 沟通详情,含 variant、messages、job 等
|
||||
* @returns {Promise<object>} { replied: true, reply_content, hr_message_text } | { replied: false, reason }
|
||||
* @param {object} options - 可选,落库用 { sn_code, platform, friendId, encryptFriendId }
|
||||
* @returns {Promise<object>} { replied, action?, reply_content?, hr_message_text?, reason? }
|
||||
*/
|
||||
async getReplyContentFromDetail(detail) {
|
||||
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;
|
||||
|
||||
// 推断 HR 与 求职者 uid
|
||||
let hr_uid = null;
|
||||
let geek_uid = null;
|
||||
|
||||
@@ -217,58 +260,62 @@ class ChatManager {
|
||||
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 (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;
|
||||
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;
|
||||
}
|
||||
|
||||
if (!last || !last.from || !hr_uid || last.from.uid !== hr_uid) {
|
||||
// 最后一条不是 HR 发的,不自动回复
|
||||
return { replied: false, reason: '最后一条不是HR消息' };
|
||||
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' };
|
||||
}
|
||||
|
||||
// 取 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({
|
||||
const { action, reply_content } = await ai_service.replyIntentAndContent({
|
||||
jobInfo,
|
||||
resumeInfo: null,
|
||||
chatType: 'reply',
|
||||
hrMessage: hr_message_text,
|
||||
previousMessages: [] // 如需上下文,这里可以把 detail.messages 映射进去
|
||||
previousMessages: hrList.slice(-5).map(m => (m.body && m.body.text) || m.pushText || '')
|
||||
});
|
||||
|
||||
if (!reply_content || !reply_content.trim()) {
|
||||
return { replied: false, reason: 'AI 未生成有效回复' };
|
||||
|
||||
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,
|
||||
reply_content,
|
||||
action: action || 'text',
|
||||
reply_content: reply_content || '',
|
||||
hr_message_text
|
||||
};
|
||||
}
|
||||
@@ -287,12 +334,25 @@ class ChatManager {
|
||||
if (!friendId) throw new Error('friendId 不能为空');
|
||||
|
||||
const parsed = await this.get_chat_detail(sn_code, mqttClient, { platform, ...detailParams });
|
||||
const decision = await this.getReplyContentFromDetail(parsed);
|
||||
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: [{ type: 'text', content: decision.reply_content }],
|
||||
messages,
|
||||
chatType: 'reply',
|
||||
platform
|
||||
});
|
||||
|
||||
@@ -189,16 +189,30 @@ class ChatHandler extends BaseHandler {
|
||||
|
||||
await this._saveChatMessagesToDb(parsed, friend, sn_code, platform_type);
|
||||
|
||||
const decision = await chatManager.getReplyContentFromDetail(parsed || {});
|
||||
const decision = await chatManager.getReplyContentFromDetail(parsed || {}, {
|
||||
sn_code,
|
||||
platform: platform_type,
|
||||
friendId: friend_id,
|
||||
encryptFriendId: friend.encryptFriendId || ''
|
||||
});
|
||||
|
||||
if (decision.replied && decision.reply_content) {
|
||||
if (decision.replied) {
|
||||
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 }];
|
||||
const actionNames = { send_resume: '发送简历', exchange_wechat: '换微信', exchange_phone: '换电话' };
|
||||
const send_command = {
|
||||
command_type: 'send_chat_message',
|
||||
command_name: '发送聊天消息',
|
||||
command_name: actionNames[action] || '发送聊天消息',
|
||||
command_params: {
|
||||
platform: platform_type,
|
||||
friendId: friend_id,
|
||||
messages: [{ type: 'text', content: decision.reply_content }],
|
||||
messages,
|
||||
chatType: 'reply'
|
||||
},
|
||||
priority: config.getTaskPriority('auto_chat') || 6
|
||||
|
||||
Reference in New Issue
Block a user