1
This commit is contained in:
@@ -363,7 +363,7 @@ module.exports = {
|
||||
* description: 账号ID
|
||||
* taskType:
|
||||
* type: string
|
||||
* description: 指令类型: get_login_qr_code-登录检查, get_resume-获取简历, search_jobs-搜索岗位
|
||||
* description: 任务类型: get_login_qr_code-登录检查, get_resume-获取简历, auto_search-搜索岗位
|
||||
* taskName:
|
||||
* type: string
|
||||
* description: 指令名称
|
||||
|
||||
@@ -69,40 +69,59 @@ class ChatManager {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析沟通详情返回值(两种形态二选一)
|
||||
* 形态1 - 会话/职位信息: zpData.data + zpData.job
|
||||
* 形态2 - 聊天消息列表: zpData.hasMore + zpData.messages
|
||||
* 解析 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 null;
|
||||
const raw = response.zpData != null ? response.zpData : response.data;
|
||||
if (!raw) return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] };
|
||||
if (!response) return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] };
|
||||
|
||||
// 形态2: 消息列表(有 messages 数组)
|
||||
if (Array.isArray(raw.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: !!raw.hasMore,
|
||||
messages: raw.messages,
|
||||
type: raw.type,
|
||||
minMsgId: raw.minMsgId
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// 形态1: 会话详情(data + job)
|
||||
if (raw.data != null || raw.job != null) {
|
||||
if (boss_zp && (boss_zp.data != null || boss_zp.job != null)) {
|
||||
return {
|
||||
variant: 'session',
|
||||
data: raw.data || null,
|
||||
job: raw.job || null
|
||||
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', ... }
|
||||
@@ -123,18 +142,17 @@ class ChatManager {
|
||||
data: rest
|
||||
});
|
||||
|
||||
const ok = response && (response.code === 0 || response.code === 200);
|
||||
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;
|
||||
}
|
||||
@@ -268,8 +286,8 @@ class ChatManager {
|
||||
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);
|
||||
const parsed = await this.get_chat_detail(sn_code, mqttClient, { platform, ...detailParams });
|
||||
const decision = await this.getReplyContentFromDetail(parsed);
|
||||
if (!decision.replied) return decision;
|
||||
|
||||
await this.send_chat_message(sn_code, mqttClient, {
|
||||
|
||||
@@ -68,99 +68,16 @@ class ChatHandler extends BaseHandler {
|
||||
return { chatCount: 0, message: '没有可沟通的会话', detail: { total_contacts: 0 } };
|
||||
}
|
||||
|
||||
this._syncFriendListToChatRecords(sn_code, platform_type, friend_list);
|
||||
|
||||
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;
|
||||
if (!friend.friendId) continue;
|
||||
const item = await this._processOneFriend(task, friend, sn_code, platform_type);
|
||||
details.push(item);
|
||||
if (item.replied) replied_count++;
|
||||
|
||||
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}`);
|
||||
@@ -175,6 +92,129 @@ class ChatHandler extends BaseHandler {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 将会话列表同步到 chat_records,内部 catch 仅打日志 */
|
||||
async _syncFriendListToChatRecords(sn_code, platform_type, friend_list) {
|
||||
try {
|
||||
const chatRecordsModel = db.getModel('chat_records');
|
||||
for (const friend of friend_list) {
|
||||
const friend_id = friend.friendId;
|
||||
if (friend_id == null) continue;
|
||||
const encryptId = friend.encryptFriendId || '';
|
||||
const existing = await chatRecordsModel.findOne({
|
||||
where: { sn_code, platform: platform_type, encryptFriendId: encryptId }
|
||||
});
|
||||
const baseData = {
|
||||
sn_code,
|
||||
platform: platform_type,
|
||||
friendId: friend_id,
|
||||
encryptFriendId: encryptId,
|
||||
name: friend.name || '',
|
||||
updateTime: friend.updateTime != null ? friend.updateTime : null,
|
||||
brandName: friend.brandName || '',
|
||||
jobName: friend.jobName || '',
|
||||
jobCity: friend.jobCity || '',
|
||||
positionName: friend.positionName || '',
|
||||
bossTitle: friend.bossTitle || '',
|
||||
friendSource: friend.friendSource != null ? friend.friendSource : 0,
|
||||
jobTypeDesc: friend.jobTypeDesc || '',
|
||||
waterLevel: friend.waterLevel != null ? friend.waterLevel : 0
|
||||
};
|
||||
if (existing) await existing.update(baseData);
|
||||
else await chatRecordsModel.create(baseData);
|
||||
}
|
||||
console.log(`[自动沟通] 已同步 ${friend_list.length} 条会话到 chat_records`);
|
||||
} catch (e) {
|
||||
console.warn('[自动沟通] 同步聊天会话列表到 chat_records 失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 落库 chat_message,入参为解析后格式 { variant, messages, data, job }
|
||||
* 有 messages 则每条一条记录;仅会话时 mid=0 存 { data, job }
|
||||
*/
|
||||
async _saveChatMessagesToDb(parsed, friend, sn_code, platform_type) {
|
||||
if (!parsed) return;
|
||||
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
||||
const has_session = parsed.data != null || parsed.job != null;
|
||||
if (messages.length === 0 && !has_session) return;
|
||||
|
||||
try {
|
||||
const chatMessageModel = db.getModel('chat_message');
|
||||
const friend_id = friend.friendId;
|
||||
const encrypt_id = friend.encryptFriendId || '';
|
||||
const fetch_time = new Date();
|
||||
const base = { sn_code, platform: platform_type, friendId: friend_id, encryptFriendId: encrypt_id, fetch_time };
|
||||
|
||||
if (messages.length > 0) {
|
||||
for (const msg of messages) {
|
||||
const mid = msg.mid != null ? msg.mid : 0;
|
||||
const existing = await chatMessageModel.findOne({
|
||||
where: { sn_code, platform: platform_type, friendId: friend_id, mid }
|
||||
});
|
||||
const row = { ...base, mid, message_data: msg };
|
||||
if (existing) await existing.update(row);
|
||||
else await chatMessageModel.create(row);
|
||||
}
|
||||
} else {
|
||||
const boss_payload = { data: parsed.data || null, job: parsed.job || null };
|
||||
const existing = await chatMessageModel.findOne({
|
||||
where: { sn_code, platform: platform_type, friendId: friend_id, mid: 0 }
|
||||
});
|
||||
const row = { ...base, mid: 0, message_data: boss_payload };
|
||||
if (existing) await existing.update(row);
|
||||
else await chatMessageModel.create(row);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[自动沟通] 写入 chat_message 失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个会话:获取详情 → 落库 → 判断是否回复 → 若需则发送消息
|
||||
* @returns {{ friendId: number, replied: boolean, reason: string|null }}
|
||||
*/
|
||||
async _processOneFriend(task, friend, sn_code, platform_type) {
|
||||
const friend_id = friend.friendId;
|
||||
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 result = detail_exec?.results?.[0]?.result;
|
||||
const parsed = chatManager.parseDetailResponse(result || {});
|
||||
|
||||
await this._saveChatMessagesToDb(parsed, friend, sn_code, platform_type);
|
||||
|
||||
const decision = await chatManager.getReplyContentFromDetail(parsed || {});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
friendId: friend_id,
|
||||
replied: !!decision.replied,
|
||||
reason: decision.reason || null
|
||||
};
|
||||
} catch (err) {
|
||||
return { friendId: friend_id, replied: false, reason: err.message || '处理失败' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChatHandler;
|
||||
|
||||
@@ -89,7 +89,7 @@ class SearchHandler extends BaseHandler {
|
||||
command_type: 'get_job_list',
|
||||
command_name: '获取职位列表',
|
||||
command_params: JSON.stringify(commandParams),
|
||||
priority: config.getTaskPriority('search_jobs') || 8
|
||||
priority: config.getTaskPriority('auto_search') || 8
|
||||
};
|
||||
|
||||
// 5. 执行搜索指令
|
||||
|
||||
@@ -29,7 +29,7 @@ class TaskHandlers {
|
||||
register(taskQueue) {
|
||||
console.log('[任务处理器] 开始注册处理器...');
|
||||
|
||||
// 注册自动搜索处理器
|
||||
// 注册自动搜索处理器(唯一搜索任务类型)
|
||||
taskQueue.registerHandler('auto_search', async (task) => {
|
||||
return await this.handleAutoSearchTask(task);
|
||||
});
|
||||
@@ -39,11 +39,6 @@ class TaskHandlers {
|
||||
return await this.handleAutoDeliverTask(task);
|
||||
});
|
||||
|
||||
// 注册搜索职位列表处理器(与 auto_search 相同)
|
||||
taskQueue.registerHandler('search_jobs', async (task) => {
|
||||
return await this.handleAutoSearchTask(task);
|
||||
});
|
||||
|
||||
// 注册自动沟通处理器
|
||||
taskQueue.registerHandler('auto_chat', async (task) => {
|
||||
return await this.handleAutoChatTask(task);
|
||||
|
||||
62
api/model/chat_message.js
Normal file
62
api/model/chat_message.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
/**
|
||||
* 聊天消息表
|
||||
* 每句话一条记录;会话形态(仅 data+job)用 mid=0 存一条
|
||||
*/
|
||||
module.exports = (db) => {
|
||||
const chat_message = db.define('chat_message', {
|
||||
sn_code: {
|
||||
comment: '设备SN码',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
},
|
||||
platform: {
|
||||
comment: '平台: boss / liepin',
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'boss'
|
||||
},
|
||||
friendId: {
|
||||
comment: '好友/会话ID',
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
encryptFriendId: {
|
||||
comment: '好友加密ID',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
mid: {
|
||||
comment: '消息ID(接口返回)',
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
message_data: {
|
||||
comment: '单条消息原始数据 JSON',
|
||||
type: Sequelize.JSON,
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
},
|
||||
fetch_time: {
|
||||
comment: '拉取时间',
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
defaultValue: Sequelize.NOW
|
||||
}
|
||||
}, {
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{ unique: true, fields: ['sn_code', 'platform', 'friendId', 'mid'] },
|
||||
{ unique: false, fields: ['sn_code', 'platform', 'friendId'] },
|
||||
{ unique: false, fields: ['fetch_time'] }
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
// chat_message.sync({ force: true });
|
||||
|
||||
return chat_message;
|
||||
};
|
||||
@@ -15,7 +15,7 @@ module.exports = (db) => {
|
||||
defaultValue: ''
|
||||
},
|
||||
taskType: {
|
||||
comment: '任务类型: get_login_qr_code-登录检查, get_resume-获取简历, search_jobs-搜索岗位, get_job_list-获取岗位列表, auto_deliver-自动投递, chat-聊天, apply-投递',
|
||||
comment: '任务类型: get_login_qr_code-登录检查, get_resume-获取简历, auto_search-搜索岗位, get_job_list-获取岗位列表, auto_deliver-自动投递, chat-聊天, apply-投递',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
|
||||
@@ -787,8 +787,8 @@ class PlaAccountService {
|
||||
const sn_code = account.sn_code;
|
||||
const platform = account.platform_type || 'boss';
|
||||
|
||||
// 2. 创建任务记录(使用新的搜索任务类型)
|
||||
const taskType = 'search_jobs';
|
||||
// 2. 创建任务记录(使用 auto_search,与定时任务统一)
|
||||
const taskType = 'auto_search';
|
||||
const taskName = autoDeliver ? '搜索并投递职位' : '搜索职位列表';
|
||||
|
||||
const task = await task_status.create({
|
||||
|
||||
Reference in New Issue
Block a user