This commit is contained in:
张成
2026-02-28 10:38:28 +08:00
parent 1a011bcc01
commit dfd3119163
44 changed files with 449 additions and 13555 deletions

View File

@@ -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
};
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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
}
};
}
}

View File

@@ -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

View File

@@ -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小时执行一次
};
}