1
This commit is contained in:
@@ -277,7 +277,7 @@ module.exports = {
|
||||
|
||||
const records = await chat_records.findAll({
|
||||
where: { jobId, sn_code },
|
||||
order: [['sendTime', 'ASC'], ['receiveTime', 'ASC'], ['id', 'ASC']]
|
||||
order: [['updateTime', 'ASC'], ['id', 'ASC']]
|
||||
});
|
||||
|
||||
return ctx.success(records);
|
||||
@@ -338,8 +338,7 @@ module.exports = {
|
||||
content,
|
||||
chatType,
|
||||
direction: 'sent',
|
||||
sendStatus: 'pending',
|
||||
sendTime: new Date()
|
||||
// 对话会话表当前不再区分单条消息时间,这里仅保留必要字段
|
||||
});
|
||||
|
||||
console.log(`[聊天管理] 消息待发送到设备 ${sn_code}:`, content);
|
||||
@@ -482,8 +481,8 @@ module.exports = {
|
||||
record.direction || '',
|
||||
record.chatType || '',
|
||||
`"${(record.content || '').replace(/"/g, '""')}"`,
|
||||
record.sendTime || '',
|
||||
record.receiveTime || '',
|
||||
'',
|
||||
'',
|
||||
record.hasReply ? '是' : '否'
|
||||
];
|
||||
csvContent += row.join(',') + '\n';
|
||||
|
||||
@@ -165,7 +165,7 @@ switch (type) {
|
||||
break;
|
||||
case 'chat':
|
||||
Model = models.chat_records;
|
||||
dateField = 'sendTime';
|
||||
dateField = 'updateTime';
|
||||
break;
|
||||
default:
|
||||
return ctx.fail('无效的统计类型');
|
||||
|
||||
@@ -69,12 +69,7 @@ module.exports = {
|
||||
}),
|
||||
chat_records.count({
|
||||
where: {
|
||||
sn_code: deviceSn,
|
||||
direction: 'sent',
|
||||
sendTime: {
|
||||
[op.gte]: todayStart,
|
||||
[op.lte]: todayEnd
|
||||
}
|
||||
sn_code: deviceSn
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('[统计] 查询聊天数量失败:', err);
|
||||
@@ -189,14 +184,9 @@ module.exports = {
|
||||
}),
|
||||
chat_records.findAll({
|
||||
where: {
|
||||
sn_code: deviceSn,
|
||||
direction: 'sent',
|
||||
sendTime: {
|
||||
[op.gte]: start,
|
||||
[op.lte]: end
|
||||
}
|
||||
sn_code: deviceSn
|
||||
},
|
||||
attributes: ['sendTime'],
|
||||
attributes: ['updateTime'],
|
||||
raw: true
|
||||
}).catch(err => {
|
||||
console.error('[统计] 查询聊天记录失败:', err);
|
||||
@@ -226,7 +216,7 @@ module.exports = {
|
||||
}).length;
|
||||
|
||||
const chatCount = allChats.filter(item => {
|
||||
const itemDate = dayjs(item.sendTime);
|
||||
const itemDate = dayjs(item.updateTime);
|
||||
return !itemDate.isBefore(dayStart, 'day') && !itemDate.isAfter(dayEnd, 'day');
|
||||
}).length;
|
||||
|
||||
@@ -499,14 +489,9 @@ module.exports = {
|
||||
|
||||
const allChats = await chat_records.findAll({
|
||||
where: {
|
||||
sn_code: deviceSn,
|
||||
direction: 'sent',
|
||||
sendTime: {
|
||||
[op.gte]: start,
|
||||
[op.lte]: end
|
||||
}
|
||||
sn_code: deviceSn
|
||||
},
|
||||
attributes: ['sendTime'],
|
||||
attributes: ['updateTime'],
|
||||
raw: true
|
||||
}).catch(err => {
|
||||
console.error('[统计] 查询聊天记录失败:', err);
|
||||
@@ -523,7 +508,7 @@ module.exports = {
|
||||
const dayEnd = currentDate.endOf('day');
|
||||
|
||||
const count = allChats.filter(item => {
|
||||
const itemDate = dayjs(item.sendTime);
|
||||
const itemDate = dayjs(item.updateTime);
|
||||
return !itemDate.isBefore(dayStart, 'day') && !itemDate.isAfter(dayEnd, 'day');
|
||||
}).length;
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ return ctx.success({
|
||||
return ctx.fail('任务不存在');
|
||||
}
|
||||
|
||||
if (task.status !== 'failed') {
|
||||
return ctx.fail('只能重试失败的任务');
|
||||
}
|
||||
|
||||
|
||||
await task_status.update({
|
||||
status: 'pending',
|
||||
|
||||
@@ -460,5 +460,82 @@ module.exports = {
|
||||
});
|
||||
return ctx.fail('保存投递配置失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据设备SN码获取账号全部功能配置(投递、沟通、活跃)
|
||||
*/
|
||||
'POST /user/account-config/get': async (ctx) => {
|
||||
try {
|
||||
const body = ctx.getBody();
|
||||
const { sn_code } = body;
|
||||
if (!sn_code) return ctx.fail('请提供设备SN码');
|
||||
|
||||
const { pla_account } = await Framework.getModels();
|
||||
const user = await pla_account.findOne({ where: { sn_code } });
|
||||
if (!user) return ctx.fail('用户不存在');
|
||||
|
||||
const u = user.toJSON ? user.toJSON() : user;
|
||||
return ctx.success({
|
||||
deliver_config: u.deliver_config || null,
|
||||
chat_strategy: u.chat_strategy || null,
|
||||
active_actions: u.active_actions || null,
|
||||
auto_chat: u.auto_chat != null ? !!u.auto_chat : false,
|
||||
auto_active: u.auto_active != null ? !!u.auto_active : false
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[获取账号配置失败]', error);
|
||||
return ctx.fail('获取账号配置失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存账号功能配置(可只传需要更新的部分:deliver_config / chat_strategy / active_actions)
|
||||
*/
|
||||
'POST /user/account-config/save': async (ctx) => {
|
||||
try {
|
||||
const body = ctx.getBody();
|
||||
const { sn_code, deliver_config, chat_strategy, active_actions } = body;
|
||||
if (!sn_code) return ctx.fail('请提供设备SN码');
|
||||
|
||||
const { pla_account } = await Framework.getModels();
|
||||
const user = await pla_account.findOne({ where: { sn_code } });
|
||||
if (!user) return ctx.fail('用户不存在');
|
||||
|
||||
const updateData = {};
|
||||
if (deliver_config !== undefined) {
|
||||
const original = user.deliver_config || {};
|
||||
const deepMerge = (t, s) => {
|
||||
const r = { ...t };
|
||||
Object.keys(s).forEach(k => {
|
||||
const sv = s[k], tv = t[k];
|
||||
if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
|
||||
r[k] = deepMerge(tv, sv);
|
||||
} else {
|
||||
r[k] = sv;
|
||||
}
|
||||
});
|
||||
return r;
|
||||
};
|
||||
updateData.deliver_config = deepMerge(original, deliver_config);
|
||||
if (deliver_config.auto_deliver !== undefined) updateData.auto_deliver = deliver_config.auto_deliver ? 1 : 0;
|
||||
else if (deliver_config.auto_delivery !== undefined) updateData.auto_deliver = deliver_config.auto_delivery ? 1 : 0;
|
||||
}
|
||||
if (chat_strategy !== undefined) {
|
||||
updateData.chat_strategy = chat_strategy;
|
||||
if (chat_strategy.auto_chat !== undefined) updateData.auto_chat = chat_strategy.auto_chat ? 1 : 0;
|
||||
}
|
||||
if (active_actions !== undefined) {
|
||||
updateData.active_actions = active_actions;
|
||||
if (active_actions.auto_active !== undefined) updateData.auto_active = active_actions.auto_active ? 1 : 0;
|
||||
}
|
||||
if (Object.keys(updateData).length === 0) return ctx.success({ message: '无更新' });
|
||||
|
||||
await pla_account.update(updateData, { where: { id: user.id } });
|
||||
return ctx.success({ message: '配置保存成功' });
|
||||
} catch (error) {
|
||||
console.error('[保存账号配置失败]', error);
|
||||
return ctx.fail('保存账号配置失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,24 @@ const ai_service = require('../../../services/ai_service');
|
||||
class ChatManager {
|
||||
/**
|
||||
* 解析沟通列表返回值,统一为 { friendList, foldText, ... }
|
||||
* 设备端可能返回 code:0 + zpData 或 code:200 + data
|
||||
* 只支持新的结构:
|
||||
* response.data = { success, apiData: [ { response: { code, zpData:{...} } } ] }
|
||||
* @private
|
||||
*/
|
||||
_parse_chat_list_response(response) {
|
||||
if (!response) return null;
|
||||
const raw = response.zpData != null ? response.zpData : response.data;
|
||||
if (!raw) return { friendList: [], foldText: '', filterEncryptIdList: [], filterBossIdList: [] };
|
||||
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 || '',
|
||||
@@ -41,14 +52,19 @@ class ChatManager {
|
||||
data: { pageCount }
|
||||
});
|
||||
|
||||
// 沟通列表接口成功为 code: 0 或 code: 200
|
||||
const ok = response && (response.code === 0 || response.code === 200);
|
||||
// 只认新结构: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;
|
||||
}
|
||||
@@ -162,37 +178,20 @@ class ChatManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 AI 自动决定是否回复,并发送回复
|
||||
* 流程:
|
||||
* 1. 根据参数获取沟通详情(消息列表)
|
||||
* 2. 如果最后一句是 HR 说的,则调用阿里云 Qwen 生成回复文案
|
||||
* 3. 通过 send_chat_message 把回复发出去
|
||||
* 根据沟通详情(get_chat_detail 的解析结果)判断是否需回复,并用 AI 生成回复文案
|
||||
* 供任务层在「获取详情」指令执行后调用,不包含发送消息(由任务层再下发 send_chat_message 指令)
|
||||
*
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} mqttClient - MQTT客户端
|
||||
* @param {object} params - 包含 friendId + 获取详情所需参数(如 encryptBossId/encryptJobId 等)
|
||||
* @returns {Promise<object>} { replied, reply_content?, hr_message_text?, reason? }
|
||||
* @param {object} detail - 沟通详情,含 variant、messages、job 等
|
||||
* @returns {Promise<object>} { replied: true, reply_content, hr_message_text } | { replied: false, reason }
|
||||
*/
|
||||
async auto_reply_with_ai(sn_code, mqttClient, params = {}) {
|
||||
const { friendId, platform = 'boss', ...detailParams } = params;
|
||||
|
||||
if (!friendId) {
|
||||
throw new Error('friendId 不能为空');
|
||||
}
|
||||
|
||||
// 1. 获取沟通详情(期望拿到消息列表)
|
||||
const detail = await this.get_chat_detail(sn_code, mqttClient, {
|
||||
platform,
|
||||
...detailParams
|
||||
});
|
||||
|
||||
async getReplyContentFromDetail(detail) {
|
||||
if (!detail || detail.variant !== 'messages' || !Array.isArray(detail.messages) || detail.messages.length === 0) {
|
||||
return { replied: false, reason: '无可用消息' };
|
||||
}
|
||||
|
||||
const messages = detail.messages;
|
||||
|
||||
// 2. 推断 HR 与 求职者 uid
|
||||
// 推断 HR 与 求职者 uid
|
||||
let hr_uid = null;
|
||||
let geek_uid = null;
|
||||
|
||||
@@ -249,14 +248,6 @@ class ChatManager {
|
||||
return { replied: false, reason: 'AI 未生成有效回复' };
|
||||
}
|
||||
|
||||
// 4. 通过统一的 send_chat_message 下发回复
|
||||
await this.send_chat_message(sn_code, mqttClient, {
|
||||
friendId,
|
||||
messages: [{ type: 'text', content: reply_content }],
|
||||
chatType: 'reply',
|
||||
platform
|
||||
});
|
||||
|
||||
return {
|
||||
replied: true,
|
||||
reply_content,
|
||||
@@ -265,78 +256,32 @@ class ChatManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动获取沟通列表 + 按会话自动 AI 回复
|
||||
* 1. 调用 get_chat_list 获取会话列表
|
||||
* 2. 对每个会话按 friendId 调用 auto_reply_with_ai(内部会先获取详情,再决定是否回复)
|
||||
* 使用 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 - { platform?, pageCount? }
|
||||
* @returns {Promise<object>} { success, total_contacts, replied_count, details: [...] }
|
||||
* @param {object} params - 包含 friendId + 获取详情所需参数
|
||||
* @returns {Promise<object>} { replied, reply_content?, hr_message_text?, reason? }
|
||||
*/
|
||||
async auto_chat_ai(sn_code, mqttClient, params = {}) {
|
||||
const { platform = 'boss', pageCount = 3 } = params;
|
||||
async auto_reply_with_ai(sn_code, mqttClient, params = {}) {
|
||||
const { friendId, platform = 'boss', ...detailParams } = params;
|
||||
if (!friendId) throw new Error('friendId 不能为空');
|
||||
|
||||
// 1. 获取沟通列表
|
||||
const listResult = await this.get_chat_list(sn_code, mqttClient, {
|
||||
platform,
|
||||
pageCount
|
||||
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
|
||||
});
|
||||
|
||||
const friendList = Array.isArray(listResult.friendList) ? listResult.friendList : [];
|
||||
|
||||
if (friendList.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
total_contacts: 0,
|
||||
replied_count: 0,
|
||||
details: [],
|
||||
message: '没有可沟通的会话'
|
||||
};
|
||||
}
|
||||
|
||||
let replied_count = 0;
|
||||
const details = [];
|
||||
|
||||
// 2. 逐个会话顺序处理,避免并发下发指令
|
||||
for (const friend of friendList) {
|
||||
const friendId = friend.friendId;
|
||||
if (!friendId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await this.auto_reply_with_ai(sn_code, mqttClient, {
|
||||
platform,
|
||||
friendId
|
||||
});
|
||||
|
||||
if (r.replied) {
|
||||
replied_count++;
|
||||
}
|
||||
|
||||
details.push({
|
||||
friendId,
|
||||
replied: !!r.replied,
|
||||
reason: r.reason || null,
|
||||
reply_content: r.reply_content || null
|
||||
});
|
||||
} catch (error) {
|
||||
details.push({
|
||||
friendId,
|
||||
replied: false,
|
||||
reason: error.message || '自动回复失败',
|
||||
reply_content: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
total_contacts: friendList.length,
|
||||
replied_count,
|
||||
details,
|
||||
message: '自动获取列表并尝试AI回复完成'
|
||||
replied: true,
|
||||
reply_content: decision.reply_content,
|
||||
hr_message_text: decision.hr_message_text
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,16 +600,16 @@ class JobManager {
|
||||
|
||||
|
||||
// 等待 1秒
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const location = await locationService.getLocationByAddress(addressToParse).catch(error => {
|
||||
console.error(`[工作管理] 获取位置失败:`, error);
|
||||
});
|
||||
// const location = await locationService.getLocationByAddress(addressToParse).catch(error => {
|
||||
// console.error(`[工作管理] 获取位置失败:`, error);
|
||||
// });
|
||||
|
||||
if (location) {
|
||||
jobInfo.latitude = String(location.lat);
|
||||
jobInfo.longitude = String(location.lng);
|
||||
}
|
||||
// if (location) {
|
||||
// jobInfo.latitude = String(location.lat);
|
||||
// jobInfo.longitude = String(location.lng);
|
||||
// }
|
||||
}
|
||||
|
||||
// 检查是否已存在(根据 jobId 和 sn_code)
|
||||
|
||||
@@ -20,6 +20,14 @@ class ScheduledJobs {
|
||||
this.taskQueue = components.taskQueue;
|
||||
this.taskHandlers = taskHandlers;
|
||||
this.jobs = [];
|
||||
|
||||
// 业务任务防重入标记(按任务类型存)
|
||||
this._runningFlags = {
|
||||
auto_search: false,
|
||||
auto_deliver: false,
|
||||
auto_chat: false,
|
||||
auto_active: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,7 +98,7 @@ class ScheduledJobs {
|
||||
console.log('[定时任务] ✓ 已启动自动投递任务 (每1分钟)');
|
||||
|
||||
// 3. 自动沟通任务 - 每15分钟执行一次
|
||||
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */15 * * * *', () => {
|
||||
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */1 * * * *', () => {
|
||||
this.runAutoChatTask();
|
||||
});
|
||||
this.jobs.push(autoChatJob);
|
||||
@@ -120,6 +128,13 @@ class ScheduledJobs {
|
||||
* 为所有启用自动搜索的账号添加搜索任务
|
||||
*/
|
||||
async runAutoSearchTask() {
|
||||
const key = 'auto_search';
|
||||
if (this._runningFlags[key]) {
|
||||
console.log('[自动搜索调度] 上一次执行尚未完成,本次跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
this._runningFlags[key] = true;
|
||||
try {
|
||||
const accounts = await this.getEnabledAccounts('auto_search');
|
||||
|
||||
@@ -146,6 +161,8 @@ class ScheduledJobs {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动搜索调度] 执行失败:', error);
|
||||
} finally {
|
||||
this._runningFlags[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +171,13 @@ class ScheduledJobs {
|
||||
* 为所有启用自动投递的账号添加投递任务
|
||||
*/
|
||||
async runAutoDeliverTask() {
|
||||
const key = 'auto_deliver';
|
||||
if (this._runningFlags[key]) {
|
||||
console.log('[自动投递调度] 上一次执行尚未完成,本次跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
this._runningFlags[key] = true;
|
||||
try {
|
||||
const accounts = await this.getEnabledAccounts('auto_deliver');
|
||||
|
||||
@@ -180,6 +204,8 @@ class ScheduledJobs {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动投递调度] 执行失败:', error);
|
||||
} finally {
|
||||
this._runningFlags[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +214,13 @@ class ScheduledJobs {
|
||||
* 为所有启用自动沟通的账号添加沟通任务
|
||||
*/
|
||||
async runAutoChatTask() {
|
||||
const key = 'auto_chat';
|
||||
if (this._runningFlags[key]) {
|
||||
console.log('[自动沟通调度] 上一次执行尚未完成,本次跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
this._runningFlags[key] = true;
|
||||
try {
|
||||
const accounts = await this.getEnabledAccounts('auto_chat');
|
||||
|
||||
@@ -214,6 +247,8 @@ class ScheduledJobs {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动沟通调度] 执行失败:', error);
|
||||
} finally {
|
||||
this._runningFlags[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +257,13 @@ class ScheduledJobs {
|
||||
* 为所有启用自动活跃的账号添加活跃任务
|
||||
*/
|
||||
async runAutoActiveTask() {
|
||||
const key = 'auto_active';
|
||||
if (this._runningFlags[key]) {
|
||||
console.log('[自动活跃调度] 上一次执行尚未完成,本次跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
this._runningFlags[key] = true;
|
||||
try {
|
||||
const accounts = await this.getEnabledAccounts('auto_active');
|
||||
|
||||
@@ -248,6 +290,8 @@ class ScheduledJobs {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动活跃调度] 执行失败:', error);
|
||||
} finally {
|
||||
this._runningFlags[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ const BaseHandler = require('./baseHandler');
|
||||
const ConfigManager = require('../services/configManager');
|
||||
const command = require('../core/command');
|
||||
const config = require('../infrastructure/config');
|
||||
const chatManager = require('../../job/managers/chatManager');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
/**
|
||||
* 自动沟通处理器
|
||||
* 负责自动回复HR消息
|
||||
* 负责自动回复 HR 消息。auto_chat 是任务,其下按指令执行:获取列表 → 获取详情 →(若需回复)发送消息
|
||||
*/
|
||||
class ChatHandler extends BaseHandler {
|
||||
/**
|
||||
@@ -24,64 +26,153 @@ class ChatHandler extends BaseHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行沟通逻辑
|
||||
* 执行沟通逻辑:先下发「获取列表」指令,再对每个会话下发「获取详情」→(若需回复)「发送消息」指令
|
||||
*/
|
||||
async doChat(task) {
|
||||
const { sn_code, taskParams } = task;
|
||||
const { platform = 'boss' } = taskParams;
|
||||
const platform = taskParams.platform || 'boss';
|
||||
|
||||
console.log(`[自动沟通] 开始 - 设备: ${sn_code}`);
|
||||
|
||||
// 1. 获取账户配置
|
||||
const accountConfig = await this.getAccountConfig(sn_code, ['platform_type', 'chat_strategy']);
|
||||
|
||||
if (!accountConfig) {
|
||||
return {
|
||||
chatCount: 0,
|
||||
message: '未找到账户配置'
|
||||
};
|
||||
return { chatCount: 0, message: '未找到账户配置' };
|
||||
}
|
||||
|
||||
// 2. 解析沟通策略配置
|
||||
const chatStrategy = ConfigManager.parseChatStrategy(accountConfig.chat_strategy);
|
||||
|
||||
// 3. 检查沟通时间范围
|
||||
const timeRange = ConfigManager.getTimeRange(chatStrategy);
|
||||
if (timeRange) {
|
||||
const timeRangeValidator = require('../services/timeRangeValidator');
|
||||
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
|
||||
|
||||
if (!timeCheck.allowed) {
|
||||
return {
|
||||
chatCount: 0,
|
||||
message: timeCheck.reason
|
||||
};
|
||||
return { chatCount: 0, message: timeCheck.reason };
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 创建自动沟通 AI 指令(内部会先获取列表,再获取详情并自动回复)
|
||||
const chatCommand = {
|
||||
command_type: 'auto_chat_ai',
|
||||
command_name: 'auto_chat_ai',
|
||||
command_params: {
|
||||
platform: platform || accountConfig.platform_type || 'boss',
|
||||
pageCount: chatStrategy.page_count || 3
|
||||
},
|
||||
const platform_type = platform || accountConfig.platform_type || 'boss';
|
||||
const page_count = chatStrategy.page_count || 3;
|
||||
|
||||
// 1. 下发「获取列表」指令
|
||||
const list_command = {
|
||||
command_type: 'get_chat_list',
|
||||
command_name: '获取聊天列表',
|
||||
command_params: { platform: platform_type, pageCount: page_count },
|
||||
priority: config.getTaskPriority('auto_chat') || 6
|
||||
};
|
||||
const list_exec = await command.executeCommands(task.id, [list_command], this.mqttClient);
|
||||
const list_result = list_exec?.results?.[0]?.result;
|
||||
const friend_list = Array.isArray(list_result?.friendList) ? list_result.friendList : [];
|
||||
|
||||
// 5. 执行指令(任务队列会保证该设备内串行执行,不并发下发指令)
|
||||
const exec_result = await command.executeCommands(task.id, [chatCommand], this.mqttClient);
|
||||
const first = exec_result && Array.isArray(exec_result.results) && exec_result.results[0]
|
||||
? exec_result.results[0].result || {}
|
||||
: {};
|
||||
if (friend_list.length === 0) {
|
||||
console.log(`[自动沟通] 完成 - 设备: ${sn_code},无会话`);
|
||||
return { chatCount: 0, message: '没有可沟通的会话', detail: { total_contacts: 0 } };
|
||||
}
|
||||
|
||||
console.log(`[自动沟通] 完成 - 设备: ${sn_code}`);
|
||||
let replied_count = 0;
|
||||
const details = [];
|
||||
|
||||
// 2. 将会话列表同步到聊天记录表(按会话维度做一条摘要记录)
|
||||
try {
|
||||
const chatRecordsModel = db.getModel('chat_records');
|
||||
for (const friend of friend_list) {
|
||||
const friend_id = friend.friendId;
|
||||
if (!friend_id) continue;
|
||||
|
||||
const encryptId = friend.encryptFriendId || '';
|
||||
|
||||
const existing = await chatRecordsModel.findOne({
|
||||
where: {
|
||||
sn_code,
|
||||
platform: platform_type,
|
||||
encryptBossId: encryptId,
|
||||
direction: 'received',
|
||||
chatType: 'session'
|
||||
}
|
||||
});
|
||||
|
||||
const baseData = {
|
||||
sn_code,
|
||||
platform: platform_type,
|
||||
encryptBossId: encryptId,
|
||||
jobTitle: friend.jobName || '',
|
||||
companyName: friend.brandName || '',
|
||||
hrName: friend.name || '',
|
||||
hrTitle: friend.bossTitle || '',
|
||||
hrId: String(friend_id),
|
||||
chatType: 'session',
|
||||
direction: 'received',
|
||||
content: '',
|
||||
contentType: 'text',
|
||||
receiveTime: friend.updateTime ? new Date(friend.updateTime) : new Date()
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await existing.update(baseData);
|
||||
} else {
|
||||
await chatRecordsModel.create(baseData);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[自动沟通] 同步聊天会话列表到 chat_records 失败:', e.message);
|
||||
}
|
||||
|
||||
// 3. 对每个会话:下发「获取详情」→ 若需回复则下发「发送消息」
|
||||
for (const friend of friend_list) {
|
||||
const friend_id = friend.friendId;
|
||||
if (!friend_id) continue;
|
||||
|
||||
try {
|
||||
const detail_command = {
|
||||
command_type: 'get_chat_detail',
|
||||
command_name: '获取聊天详情',
|
||||
command_params: { platform: platform_type, friendId: friend_id },
|
||||
priority: config.getTaskPriority('auto_chat') || 6
|
||||
};
|
||||
const detail_exec = await command.executeCommands(task.id, [detail_command], this.mqttClient);
|
||||
const detail = detail_exec?.results?.[0]?.result;
|
||||
|
||||
const decision = await chatManager.getReplyContentFromDetail(detail || {});
|
||||
|
||||
if (decision.replied && decision.reply_content) {
|
||||
const send_command = {
|
||||
command_type: 'send_chat_message',
|
||||
command_name: '发送聊天消息',
|
||||
command_params: {
|
||||
platform: platform_type,
|
||||
friendId: friend_id,
|
||||
messages: [{ type: 'text', content: decision.reply_content }],
|
||||
chatType: 'reply'
|
||||
},
|
||||
priority: config.getTaskPriority('auto_chat') || 6
|
||||
};
|
||||
await command.executeCommands(task.id, [send_command], this.mqttClient);
|
||||
replied_count++;
|
||||
}
|
||||
|
||||
details.push({
|
||||
friendId: friend_id,
|
||||
replied: !!decision.replied,
|
||||
reason: decision.reason || null
|
||||
});
|
||||
} catch (err) {
|
||||
details.push({
|
||||
friendId: friend_id,
|
||||
replied: false,
|
||||
reason: err.message || '处理失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[自动沟通] 完成 - 设备: ${sn_code},会话 ${friend_list.length},回复 ${replied_count}`);
|
||||
|
||||
return {
|
||||
chatCount: first.replied_count || 0,
|
||||
message: first.message || '自动沟通完成',
|
||||
detail: first
|
||||
chatCount: replied_count,
|
||||
message: '自动沟通完成',
|
||||
detail: {
|
||||
total_contacts: friend_list.length,
|
||||
replied_count,
|
||||
details
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,16 +279,22 @@ class DeliverHandler extends BaseHandler {
|
||||
*/
|
||||
mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig) {
|
||||
// 排除关键词
|
||||
const jobTypeExclude = jobTypeConfig?.excludeKeywords
|
||||
const rawJobTypeExclude = jobTypeConfig?.excludeKeywords
|
||||
? ConfigManager.parseConfig(jobTypeConfig.excludeKeywords, [])
|
||||
: [];
|
||||
|
||||
const deliverExclude = ConfigManager.getExcludeKeywords(deliverConfig);
|
||||
const filterExclude = filterRules.excludeKeywords || [];
|
||||
const jobTypeExclude = Array.isArray(rawJobTypeExclude) ? rawJobTypeExclude : [];
|
||||
|
||||
const deliverExcludeRaw = ConfigManager.getExcludeKeywords(deliverConfig);
|
||||
const deliverExclude = Array.isArray(deliverExcludeRaw) ? deliverExcludeRaw : [];
|
||||
const filterExcludeRaw = filterRules.excludeKeywords || [];
|
||||
const filterExclude = Array.isArray(filterExcludeRaw) ? filterExcludeRaw : [];
|
||||
|
||||
// 过滤关键词
|
||||
const deliverFilter = ConfigManager.getFilterKeywords(deliverConfig);
|
||||
const filterKeywords = filterRules.keywords || [];
|
||||
const deliverFilterRaw = ConfigManager.getFilterKeywords(deliverConfig);
|
||||
const deliverFilter = Array.isArray(deliverFilterRaw) ? deliverFilterRaw : [];
|
||||
const filterKeywordsRaw = filterRules.keywords || [];
|
||||
const filterKeywords = Array.isArray(filterKeywordsRaw) ? filterKeywordsRaw : [];
|
||||
|
||||
// 薪资范围
|
||||
const salaryRange = filterRules.minSalary || filterRules.maxSalary
|
||||
|
||||
@@ -50,7 +50,7 @@ class ScheduleConfig {
|
||||
monitoringInterval: '*/1 * * * *', // 监控检查间隔:1分钟
|
||||
autoSearch: '0 0 */1 * * *', // 自动搜索任务:每1小时执行一次
|
||||
autoDeliver: '0 */2 * * * *', // 自动投递任务:每2分钟执行一次
|
||||
autoChat: '0 */15 * * * *', // 自动沟通任务:每15分钟执行一次
|
||||
autoChat: '0 */1 * * * *', // 自动沟通任务:每1分钟执行一次
|
||||
autoActive: '0 0 */2 * * *' // 自动活跃任务:每2小时执行一次
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
/**
|
||||
* 聊天记录表模型
|
||||
* 记录与HR的聊天内容和效果
|
||||
* 聊天会话列表表模型
|
||||
* 按 Boss 「好友/会话」列表维度存储,会话摘要信息
|
||||
*/
|
||||
module.exports = (db) => {
|
||||
const chat_records = db.define("chat_records", {
|
||||
// 聊天基本信息
|
||||
const chat_records = db.define('chat_records', {
|
||||
// 设备与平台
|
||||
sn_code: {
|
||||
comment: '设备SN码',
|
||||
type: Sequelize.STRING(50),
|
||||
@@ -19,270 +19,91 @@ module.exports = (db) => {
|
||||
allowNull: false,
|
||||
defaultValue: 'boss'
|
||||
},
|
||||
|
||||
// 岗位关联
|
||||
jobId: {
|
||||
comment: '岗位ID',
|
||||
|
||||
// Boss 会话列表字段(与接口 friend 对象对应)
|
||||
friendId: {
|
||||
comment: '好友ID',
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
friendSource: {
|
||||
comment: '好友来源',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0
|
||||
},
|
||||
encryptFriendId: {
|
||||
comment: '好友加密ID',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
},
|
||||
encryptBossId: {
|
||||
comment: 'Boss加密ID',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
jobTitle: {
|
||||
comment: '岗位名称',
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
companyName: {
|
||||
comment: '公司名称',
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
|
||||
// HR信息
|
||||
hrName: {
|
||||
name: {
|
||||
comment: 'HR姓名',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
hrTitle: {
|
||||
comment: 'HR职位',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
hrId: {
|
||||
comment: 'HR ID',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
|
||||
// 聊天内容
|
||||
chatType: {
|
||||
comment: '聊天类型: greeting-打招呼, follow_up-跟进, interview-面试邀约, reply-回复',
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'greeting'
|
||||
},
|
||||
direction: {
|
||||
comment: '消息方向: sent-发送, received-接收',
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'sent'
|
||||
},
|
||||
content: {
|
||||
comment: '聊天内容',
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
},
|
||||
contentType: {
|
||||
comment: '内容类型: text-文本, image-图片, file-文件',
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: true,
|
||||
defaultValue: 'text'
|
||||
},
|
||||
|
||||
// AI生成信息
|
||||
isAiGenerated: {
|
||||
comment: '是否AI生成',
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: false
|
||||
},
|
||||
aiPrompt: {
|
||||
comment: 'AI生成的提示词',
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
aiModel: {
|
||||
comment: 'AI模型名称',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
|
||||
// 发送状态
|
||||
sendStatus: {
|
||||
comment: '发送状态: pending-待发送, sending-发送中, sent-已发送, failed-发送失败',
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: true,
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
sendTime: {
|
||||
comment: '发送时间',
|
||||
type: Sequelize.DATE,
|
||||
updateTime: {
|
||||
comment: '最后更新时间(毫秒时间戳)',
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: true
|
||||
},
|
||||
receiveTime: {
|
||||
comment: '接收时间',
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
|
||||
// 回复信息
|
||||
hasReply: {
|
||||
comment: '是否有回复',
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: false
|
||||
},
|
||||
replyTime: {
|
||||
comment: '回复时间',
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
replyContent: {
|
||||
comment: '回复内容',
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
replyDuration: {
|
||||
comment: '回复时长(分钟)',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0
|
||||
},
|
||||
|
||||
// 面试邀约信息
|
||||
isInterviewInvitation: {
|
||||
comment: '是否包含面试邀约',
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: false
|
||||
},
|
||||
interviewTime: {
|
||||
comment: '面试时间',
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
interviewLocation: {
|
||||
comment: '面试地点',
|
||||
brandName: {
|
||||
comment: '公司名称',
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
interviewType: {
|
||||
comment: '面试类型: online-线上, offline-线下',
|
||||
type: Sequelize.STRING(20),
|
||||
jobName: {
|
||||
comment: '职位名称',
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
interviewStatus: {
|
||||
comment: '面试状态: pending-待确认, confirmed-已确认, completed-已完成, cancelled-已取消',
|
||||
type: Sequelize.STRING(20),
|
||||
jobTypeDesc: {
|
||||
comment: '职位类型描述',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
|
||||
// 效果评估
|
||||
effectScore: {
|
||||
comment: '效果评分(0-100)',
|
||||
jobCity: {
|
||||
comment: '职位城市',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
positionName: {
|
||||
comment: '岗位名称/方向',
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
bossTitle: {
|
||||
comment: 'Boss/HR 职位头衔',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
waterLevel: {
|
||||
comment: '水位(Boss 优先级标记)',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0
|
||||
},
|
||||
sentiment: {
|
||||
comment: '情感倾向: positive-正面, neutral-中性, negative-负面',
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: true,
|
||||
defaultValue: 'neutral'
|
||||
},
|
||||
|
||||
// 会话信息
|
||||
conversationId: {
|
||||
comment: '会话ID(同一个岗位的聊天属于一个会话)',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
messageIndex: {
|
||||
comment: '消息序号(会话内的消息顺序)',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0
|
||||
},
|
||||
|
||||
// 关联信息
|
||||
taskId: {
|
||||
comment: '关联任务ID',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
|
||||
// 其他信息
|
||||
originalData: {
|
||||
comment: '原始数据(JSON)',
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
errorMessage: {
|
||||
comment: '错误信息',
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
notes: {
|
||||
comment: '备注',
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
}, {
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{
|
||||
unique: false,
|
||||
fields: ['sn_code']
|
||||
},
|
||||
{
|
||||
unique: false,
|
||||
fields: ['jobId']
|
||||
},
|
||||
{
|
||||
unique: false,
|
||||
fields: ['encryptBossId']
|
||||
},
|
||||
{
|
||||
unique: false,
|
||||
fields: ['conversationId']
|
||||
},
|
||||
{
|
||||
unique: false,
|
||||
fields: ['chatType']
|
||||
},
|
||||
{
|
||||
unique: false,
|
||||
fields: ['sendStatus']
|
||||
},
|
||||
{
|
||||
unique: false,
|
||||
fields: ['hasReply']
|
||||
},
|
||||
|
||||
{ unique: false, fields: ['sn_code'] },
|
||||
{ unique: false, fields: ['platform'] },
|
||||
{ unique: false, fields: ['friendId'] },
|
||||
{ unique: false, fields: ['encryptFriendId'] }
|
||||
]
|
||||
});
|
||||
|
||||
//chat_records.sync({ force: true });
|
||||
// chat_records.sync({ force: true });
|
||||
|
||||
return chat_records
|
||||
|
||||
|
||||
|
||||
return chat_records;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user