From 048c40d802e2283725a8c49e3340a2a52a3ab760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Wed, 8 Apr 2026 15:28:02 +0800 Subject: [PATCH] 1 --- api/middleware/job/managers/jobManager.js | 8 ++ api/middleware/schedule/core/scheduledJobs.js | 89 ++++++++++++- .../schedule/infrastructure/config.js | 3 +- api/model/job_types.js | 10 ++ api/services/job_type_ai_sync_service.js | 123 ++++++++++++++++++ 5 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 api/services/job_type_ai_sync_service.js diff --git a/api/middleware/job/managers/jobManager.js b/api/middleware/job/managers/jobManager.js index e9a5255..cbff7fa 100644 --- a/api/middleware/job/managers/jobManager.js +++ b/api/middleware/job/managers/jobManager.js @@ -421,6 +421,14 @@ class JobManager { } const list = Array.isArray(response.data) ? response.data : []; 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; } diff --git a/api/middleware/schedule/core/scheduledJobs.js b/api/middleware/schedule/core/scheduledJobs.js index b362d66..dce5bda 100644 --- a/api/middleware/schedule/core/scheduledJobs.js +++ b/api/middleware/schedule/core/scheduledJobs.js @@ -26,7 +26,8 @@ class ScheduledJobs { auto_search: false, auto_deliver: 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); 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(() => { console.log('[定时任务] 立即执行一次初始化任务...'); this.runAutoDeliverTask(); this.runAutoChatTask(); + this.runDailyJobTypeListingsAiSync().catch((err) => { + console.error('[定时任务] 启动时 job_types AI 同步失败:', err); + }); }, 10000); // 延迟10秒,等待系统初始化完成和设备心跳 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 diff --git a/api/middleware/schedule/infrastructure/config.js b/api/middleware/schedule/infrastructure/config.js index 63c9942..ab08d6f 100644 --- a/api/middleware/schedule/infrastructure/config.js +++ b/api/middleware/schedule/infrastructure/config.js @@ -51,7 +51,8 @@ class ScheduleConfig { autoSearch: '0 0 */1 * * *', // 自动搜索任务:每1小时执行一次 autoDeliver: '0 */2 * * * *', // 自动投递任务:每2分钟执行一次 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 }; } diff --git a/api/model/job_types.js b/api/model/job_types.js index b12c062..dc3a58a 100644 --- a/api/model/job_types.js +++ b/api/model/job_types.js @@ -41,6 +41,12 @@ module.exports = (db) => { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 + }, + pla_account_id: { + comment: '关联账户ID(pla_account.id,可选;AI 根据 get_job_listings 更新本行时写入)', + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null } }, { timestamps: false, @@ -52,6 +58,10 @@ module.exports = (db) => { { unique: false, fields: ['name'] + }, + { + unique: false, + fields: ['pla_account_id'] } ] }); diff --git a/api/services/job_type_ai_sync_service.js b/api/services/job_type_ai_sync_service.js new file mode 100644 index 0000000..5a15ca0 --- /dev/null +++ b/api/services/job_type_ai_sync_service.js @@ -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 +};