1
This commit is contained in:
@@ -421,6 +421,14 @@ class JobManager {
|
|||||||
}
|
}
|
||||||
const list = Array.isArray(response.data) ? response.data : [];
|
const list = Array.isArray(response.data) ? response.data : [];
|
||||||
console.log(`[工作管理] 获取 job_listings 成功,共 ${list.length} 个 tab`);
|
console.log(`[工作管理] 获取 job_listings 成功,共 ${list.length} 个 tab`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jobTypeAiSyncService = require('../../../services/job_type_ai_sync_service');
|
||||||
|
await jobTypeAiSyncService.maybeSyncAfterListings(sn_code, list, platform);
|
||||||
|
} catch (syncErr) {
|
||||||
|
console.warn('[工作管理] job_types AI 同步失败:', syncErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class ScheduledJobs {
|
|||||||
auto_search: false,
|
auto_search: false,
|
||||||
auto_deliver: false,
|
auto_deliver: false,
|
||||||
auto_chat: false,
|
auto_chat: false,
|
||||||
auto_active: false
|
auto_active: false,
|
||||||
|
job_type_listings_ai: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +112,23 @@ class ScheduledJobs {
|
|||||||
this.jobs.push(autoActiveJob);
|
this.jobs.push(autoActiveJob);
|
||||||
console.log('[定时任务] ✓ 已启动自动活跃任务 (每2小时)');
|
console.log('[定时任务] ✓ 已启动自动活跃任务 (每2小时)');
|
||||||
|
|
||||||
|
// 5. 每日拉取 get_job_listings 并用 AI 更新 job_types(description / excludeKeywords / commonSkills)
|
||||||
|
const jobTypeListingsAiJob = node_schedule.scheduleJob(config.schedules.jobTypeListingsAi || '0 0 4 * * *', () => {
|
||||||
|
this.runDailyJobTypeListingsAiSync().catch((err) => {
|
||||||
|
console.error('[定时任务] 每日 job_types AI 同步失败:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.jobs.push(jobTypeListingsAiJob);
|
||||||
|
console.log('[定时任务] ✓ 已启动每日 job_types AI 同步 (每天 04:00)');
|
||||||
|
|
||||||
// 立即执行一次业务任务(可选)
|
// 立即执行一次业务任务(可选)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('[定时任务] 立即执行一次初始化任务...');
|
console.log('[定时任务] 立即执行一次初始化任务...');
|
||||||
this.runAutoDeliverTask();
|
this.runAutoDeliverTask();
|
||||||
this.runAutoChatTask();
|
this.runAutoChatTask();
|
||||||
|
this.runDailyJobTypeListingsAiSync().catch((err) => {
|
||||||
|
console.error('[定时任务] 启动时 job_types AI 同步失败:', err);
|
||||||
|
});
|
||||||
}, 10000); // 延迟10秒,等待系统初始化完成和设备心跳
|
}, 10000); // 延迟10秒,等待系统初始化完成和设备心跳
|
||||||
|
|
||||||
console.log('[定时任务] 所有定时任务启动完成!');
|
console.log('[定时任务] 所有定时任务启动完成!');
|
||||||
@@ -295,6 +308,80 @@ class ScheduledJobs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日一次:对已绑定 job_type_id 且设备在线的账号下发 get_job_listings,成功后在 jobManager 内触发 AI 更新 job_types
|
||||||
|
*/
|
||||||
|
async runDailyJobTypeListingsAiSync() {
|
||||||
|
const key = 'job_type_listings_ai';
|
||||||
|
if (this._runningFlags[key]) {
|
||||||
|
console.log('[job_type_listings_ai] 上一次执行尚未完成,本次跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._runningFlags[key] = true;
|
||||||
|
try {
|
||||||
|
const Sequelize = require('sequelize');
|
||||||
|
const { Op } = Sequelize;
|
||||||
|
const scheduleManager = require('../index');
|
||||||
|
const jobApi = require('../../job/index');
|
||||||
|
const mqtt = scheduleManager.mqttClient;
|
||||||
|
if (!mqtt) {
|
||||||
|
console.warn('[job_type_listings_ai] MQTT 未初始化,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pla_account } = db.models;
|
||||||
|
const accounts = await pla_account.findAll({
|
||||||
|
where: {
|
||||||
|
is_delete: 0,
|
||||||
|
is_enabled: 1,
|
||||||
|
job_type_id: { [Op.ne]: null }
|
||||||
|
},
|
||||||
|
attributes: ['id', 'sn_code', 'job_type_id', 'platform_type']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const offlineThreshold = 3 * 60 * 1000;
|
||||||
|
let ok = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for (const acc of accounts) {
|
||||||
|
const sn_code = acc.sn_code;
|
||||||
|
const device = deviceManager.devices.get(sn_code);
|
||||||
|
const lastHb = device && device.lastHeartbeat ? device.lastHeartbeat : 0;
|
||||||
|
const isOnline = device && device.isOnline && now - lastHb < offlineThreshold;
|
||||||
|
if (!isOnline) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform =
|
||||||
|
acc.platform_type ||
|
||||||
|
(typeof acc.getDataValue === 'function' && acc.getDataValue('platform_type')) ||
|
||||||
|
'boss';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await jobApi.get_job_listings(sn_code, mqtt, { platform });
|
||||||
|
ok++;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[job_type_listings_ai] 设备 ${sn_code} 失败:`, err.message);
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok > 0 || skipped > 0) {
|
||||||
|
console.log(`[job_type_listings_ai] 完成: 成功 ${ok},跳过/失败 ${skipped},共 ${accounts.length} 个账号`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[job_type_listings_ai] 执行失败:', error);
|
||||||
|
} finally {
|
||||||
|
this._runningFlags[key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取启用指定功能的账号列表
|
* 获取启用指定功能的账号列表
|
||||||
* @param {string} featureType - 功能类型: auto_search, auto_deliver, auto_chat, auto_active
|
* @param {string} featureType - 功能类型: auto_search, auto_deliver, auto_chat, auto_active
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ class ScheduleConfig {
|
|||||||
autoSearch: '0 0 */1 * * *', // 自动搜索任务:每1小时执行一次
|
autoSearch: '0 0 */1 * * *', // 自动搜索任务:每1小时执行一次
|
||||||
autoDeliver: '0 */2 * * * *', // 自动投递任务:每2分钟执行一次
|
autoDeliver: '0 */2 * * * *', // 自动投递任务:每2分钟执行一次
|
||||||
autoChat: '0 */1 * * * *', // 自动沟通任务:每1分钟执行一次
|
autoChat: '0 */1 * * * *', // 自动沟通任务:每1分钟执行一次
|
||||||
autoActive: '0 0 */2 * * *' // 自动活跃任务:每2小时执行一次
|
autoActive: '0 0 */2 * * *', // 自动活跃任务:每2小时执行一次
|
||||||
|
jobTypeListingsAi: '0 0 4 * * *' // 每天 04:00 对有 job_type_id 的在线设备拉取 get_job_listings 并 AI 更新 job_types
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ module.exports = (db) => {
|
|||||||
type: Sequelize.INTEGER,
|
type: Sequelize.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
pla_account_id: {
|
||||||
|
comment: '关联账户ID(pla_account.id,可选;AI 根据 get_job_listings 更新本行时写入)',
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
@@ -52,6 +58,10 @@ module.exports = (db) => {
|
|||||||
{
|
{
|
||||||
unique: false,
|
unique: false,
|
||||||
fields: ['name']
|
fields: ['name']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unique: false,
|
||||||
|
fields: ['pla_account_id']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
123
api/services/job_type_ai_sync_service.js
Normal file
123
api/services/job_type_ai_sync_service.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* 根据 get_job_listings 返回的 Tab 列表,用 AI 更新 job_types 的 description / excludeKeywords / commonSkills
|
||||||
|
*/
|
||||||
|
const db = require('../middleware/dbProxy');
|
||||||
|
const aiService = require('./ai_service');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从模型回复中解析 JSON 对象
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
function parseJsonFromAi(text) {
|
||||||
|
if (!text || typeof text !== 'string') return null;
|
||||||
|
let s = text.trim();
|
||||||
|
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
||||||
|
if (fence) s = fence[1].trim();
|
||||||
|
try {
|
||||||
|
return JSON.parse(s);
|
||||||
|
} catch (e) {
|
||||||
|
const m = s.match(/\{[\s\S]*\}/);
|
||||||
|
if (m) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(m[0]);
|
||||||
|
} catch (e2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功拉取 job_listings 后:若账号绑定了 job_type_id,则用 AI 更新对应 job_types 行
|
||||||
|
* @param {string} sn_code
|
||||||
|
* @param {Array<{ index?: number, text?: string }>} tabs
|
||||||
|
* @param {string} platform
|
||||||
|
* @returns {Promise<{ updated: boolean, jobTypeId?: number }|null>}
|
||||||
|
*/
|
||||||
|
async function maybeSyncAfterListings(sn_code, tabs, platform = 'boss') {
|
||||||
|
if (!sn_code) return null;
|
||||||
|
|
||||||
|
const pla_account = db.getModel('pla_account');
|
||||||
|
const account = await pla_account.findOne({
|
||||||
|
where: { sn_code, is_delete: 0 }
|
||||||
|
});
|
||||||
|
if (!account || !account.job_type_id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const job_types = db.getModel('job_types');
|
||||||
|
const jobType = await job_types.findByPk(account.job_type_id);
|
||||||
|
if (!jobType) {
|
||||||
|
console.warn('[job_type_ai_sync] job_types 不存在, id=', account.job_type_id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabTexts = (tabs || [])
|
||||||
|
.map((t) => (t && t.text != null ? String(t.text).trim() : ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (tabTexts.length === 0) {
|
||||||
|
console.warn('[job_type_ai_sync] Tab 列表为空,跳过 AI 更新');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeName = jobType.name || '';
|
||||||
|
const labelsStr = tabTexts.join('、');
|
||||||
|
|
||||||
|
const prompt = `你是招聘平台求职助手。用户在某招聘网站的「期望职类/Tab」列表如下(按顺序):
|
||||||
|
${labelsStr}
|
||||||
|
|
||||||
|
当前在系统中登记的职位类型名称为:「${typeName}」
|
||||||
|
平台标识:${platform}
|
||||||
|
|
||||||
|
请根据上述 Tab 名称,补充该求职方向的说明、自动投递时应排除的岗位关键词、以及该方向常见技能关键词。
|
||||||
|
|
||||||
|
请只输出一段 JSON(不要 Markdown 代码块,不要其它说明),格式严格如下:
|
||||||
|
{"description":"50~200字中文,概括该求职方向","excludeKeywords":["关键词1","关键词2"],"commonSkills":["技能1","技能2"]}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
- description:面向求职者的简短说明。
|
||||||
|
- excludeKeywords:5~12 个字符串,用于过滤明显不合适的岗位(如用户做研发可排除「纯销售」「客服」等,按 Tab 语义推断)。
|
||||||
|
- commonSkills:8~20 个字符串,该方向常见技能或技术栈关键词,用于匹配加分。
|
||||||
|
- 所有字符串使用中文或业界通用英文技术词均可。`;
|
||||||
|
|
||||||
|
const { content } = await aiService.callAPI(prompt, {
|
||||||
|
systemPrompt: '你只输出合法 JSON 对象,键为 description、excludeKeywords、commonSkills,不要输出其它文字。',
|
||||||
|
temperature: 0.35,
|
||||||
|
maxTokens: 2000,
|
||||||
|
sn_code,
|
||||||
|
service_type: 'job_type_sync',
|
||||||
|
business_type: 'job_type_listings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = parseJsonFromAi(content);
|
||||||
|
if (!parsed || typeof parsed !== 'object') {
|
||||||
|
throw new Error('AI 返回无法解析为 JSON');
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = String(parsed.description || '').slice(0, 4000);
|
||||||
|
const excludeArr = Array.isArray(parsed.excludeKeywords) ? parsed.excludeKeywords.map(String) : [];
|
||||||
|
const skillsArr = Array.isArray(parsed.commonSkills) ? parsed.commonSkills.map(String) : [];
|
||||||
|
|
||||||
|
await job_types.update(
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
excludeKeywords: JSON.stringify(excludeArr),
|
||||||
|
commonSkills: JSON.stringify(skillsArr),
|
||||||
|
pla_account_id: account.id
|
||||||
|
},
|
||||||
|
{ where: { id: jobType.id } }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[job_type_ai_sync] 已更新 job_types id=${jobType.id} pla_account_id=${account.id} exclude=${excludeArr.length} skills=${skillsArr.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { updated: true, jobTypeId: jobType.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
maybeSyncAfterListings,
|
||||||
|
parseJsonFromAi
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user