const fs = require('fs'); const path = require('path'); const axios = require('axios'); const cheerio = require('cheerio'); const https = require('https'); const crypto = require('crypto'); const TARGET_URL = 'https://mybank.icbc.com.cn/icbc/newperbank/perbank3/gold/goldaccrual_query_out.jsp'; const PRICE_SELECTOR = '#activeprice_080020000521'; const DATA_FILE = path.join(__dirname, 'gold_data.json'); // 自定义 HTTPS Agent:允许不安全证书,并启用旧版 SSL 重协商兼容 const insecureHttpsAgent = new https.Agent({ rejectUnauthorized: false, secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT || 0, }); // ServerChan Key 配置:优先使用直接设置,否则使用环境变量 // 支持多个key,使用分号(;)分割 const SERVER_CHAN_KEY_DIRECT = ''; // 直接设置 ServerChan Key,有值时禁用环境变量,多个key用分号分割 const SERVER_CHAN_KEY_RAW = SERVER_CHAN_KEY_DIRECT || process.env.PUSH_KEYT || ''; const SERVER_CHAN_KEYS = SERVER_CHAN_KEY_RAW.split(';').map(k => k.trim()).filter(k => k.length > 0); // 可自定义的特殊提醒价格数组:[价格1,价格2,价格3,价格4,价格5,价格6,价格7] const SPECIAL_PRICE_TARGETS = [1000,1010,1020,1030,1040,1050,1060,1070,1080,1090,1100,1110,1120,1130,1140,1150,1160,1170,1180,1190,1200,1210,1220,1230,1240,1250,1260,1270,1280,1290,1300,1310,1320,1330,1340,1350,1360,1370,1380,1390,1400,1410,1420,1430,1440,1450,1460,1470,1480,1490,1500]; // 可配置项:变化量阈值(元/克)、变化计算窗口(分钟)、采样间隔(分钟)、价格精度(小数位数) const CONFIG = { changeThresholdYuan: 1.0, changeWindowMinutes: 60, sampleIntervalMinutes: 1, pricePrecisionDigits: 2, enableTimedPush: false, // 定时推送开关(变化量触发和收盘推送) enableSpecialAlert: true, // 特殊价格提醒开关 pushIntervalMinutes: 60, // 推送间隔(分钟),防止频繁推送 forcePushTest: false, // 测试强制推送:为 true 时每次运行必推送一次并立即退出 // 禁用推送时间段,格式为 8 位数字 [HHMMHHMM],如 10301230 表示 10:30-12:30 disabledWindows: [12001500], // 特殊价格强制推送:为 true 时命中特殊价将忽略禁用时间段 enableSpecialForcePush: true, // 特殊价格忽略推送间隔:为 true 时命中特殊价将忽略 pushIntervalMinutes 限制,即忽略普通推送时间限制 enableSpecialBypassInterval: true, // 单个价格冷却时间的默认值(分钟),当某个价格未在 specialPriceCoolDownMinutes 中单独配置时使用 coolDownMinutesForSinglePrice: 90, // 命中某个特殊价格时,是否清零其他价格的冷却时间(价格 a 冷却中,如果价格 b 触发,则清零 a 的冷却) resetOtherSpecialCooldownOnHit: true, // 每个特殊价格的单独冷却时间(分钟),key 为价格,value 为分钟数;未配置则使用 coolDownMinutesForSinglePrice 作为默认值 specialPriceCoolDownMinutes: { }, }; function getSpecialPriceCoolDownMinutes(price) { if (!price && price !== 0) return 0; const map = CONFIG.specialPriceCoolDownMinutes || {}; const key = String(price); if (Object.prototype.hasOwnProperty.call(map, key)) { return Number(map[key]) || 0; } return CONFIG.coolDownMinutesForSinglePrice || 0; } function readData() { try { if (!fs.existsSync(DATA_FILE)) return { priceHistory: [], closingPrices: [], openingPrices: [], specialTargets: SPECIAL_PRICE_TARGETS, lastPushTs: 0, lastNormalPushTs: 0, lastSpecialPricePush: {} }; const raw = fs.readFileSync(DATA_FILE, 'utf-8'); const data = JSON.parse(raw || '{}'); if (!Array.isArray(data.priceHistory)) data.priceHistory = []; if (!Array.isArray(data.closingPrices)) data.closingPrices = []; if (!Array.isArray(data.openingPrices)) data.openingPrices = []; if (!Array.isArray(data.specialTargets)) data.specialTargets = SPECIAL_PRICE_TARGETS; if (typeof data.lastPushTs !== 'number') data.lastPushTs = 0; if (typeof data.lastNormalPushTs !== 'number') data.lastNormalPushTs = 0; if (!data.lastSpecialPricePush || typeof data.lastSpecialPricePush !== 'object') data.lastSpecialPricePush = {}; return data; } catch { return { priceHistory: [], closingPrices: [], openingPrices: [], specialTargets: SPECIAL_PRICE_TARGETS, lastPushTs: 0, lastNormalPushTs: 0, lastSpecialPricePush: {} }; } } function writeData(data) { const safe = { priceHistory: Array.isArray(data.priceHistory) ? data.priceHistory : [], closingPrices: Array.isArray(data.closingPrices) ? data.closingPrices : [], openingPrices: Array.isArray(data.openingPrices) ? data.openingPrices : [], specialTargets: Array.isArray(data.specialTargets) ? data.specialTargets : SPECIAL_PRICE_TARGETS, lastPushTs: typeof data.lastPushTs === 'number' ? data.lastPushTs : 0, lastNormalPushTs: typeof data.lastNormalPushTs === 'number' ? data.lastNormalPushTs : 0, lastSpecialPricePush: data.lastSpecialPricePush && typeof data.lastSpecialPricePush === 'object' ? data.lastSpecialPricePush : {}, }; fs.writeFileSync(DATA_FILE, JSON.stringify(safe, null, 2), 'utf-8'); } function nowTs() { return Date.now(); } function toCNDate(ts = Date.now()) { const d = new Date(ts + 8 * 60 * 60 * 1000); const y = d.getUTCFullYear(); const m = String(d.getUTCMonth() + 1).padStart(2, '0'); const day = String(d.getUTCDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } function toCNTimeParts(ts = Date.now()) { const d = new Date(ts + 8 * 60 * 60 * 1000); const hh = d.getUTCHours(); const mm = d.getUTCMinutes(); return { hh, mm }; } function isInDisabledWindow(ts = Date.now()) { if (!Array.isArray(CONFIG.disabledWindows) || CONFIG.disabledWindows.length === 0) return false; const { hh, mm } = toCNTimeParts(ts); const hhmm = hh * 100 + mm; // e.g., 10:30 -> 1030 for (const win of CONFIG.disabledWindows) { const num = Number(win); if (!Number.isFinite(num) || String(Math.abs(num)).length < 7) continue; const start = Math.floor(num / 10000); // 前四位 HHMM const end = num % 10000; // 后四位 HHMM if (start <= hhmm && hhmm <= end) return true; } return false; } async function fetchCurrentPrice() { console.log('[gold] fetching html to parse price...'); const resp = await axios.get(TARGET_URL, { timeout: 60000, httpsAgent: insecureHttpsAgent, headers: { // 部分站点会根据 UA 返回不同内容,给一个常见 UA 更稳 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', }, // 某些服务端会返回非 200 但仍有 HTML,这里保持默认即可;若后续遇到问题再放开 validateStatus }); const html = resp && resp.data ? String(resp.data) : ''; if (!html) throw new Error('目标页面返回空内容'); const $ = cheerio.load(html); const text = $(PRICE_SELECTOR).text().trim(); const numeric = parseFloat(String(text).replace(/[^0-9.\-]/g, '')); if (!isFinite(numeric)) { throw new Error(`无法解析价格,selector=${PRICE_SELECTOR},text=${JSON.stringify(text)}`); } console.log(`[gold] fetched current price: ${numeric}`); return numeric; } function findDeltaWithin(history, minutes) { if (!history.length) return null; const now = nowTs(); const targetMs = minutes * 60 * 1000; const cutoff = now - targetMs; let candidate = null; for (let i = history.length - 1; i >= 0; i--) { const item = history[i]; if (item.ts <= cutoff) { candidate = item; break; } } return candidate; } function computeTrendType(values) { if (values.length < 2) return '数据不足'; const n = values.length; let sumX = 0; let sumY = 0; let sumXY = 0; let sumXX = 0; for (let i = 0; i < n; i++) { const x = i + 1; const y = values[i]; sumX += x; sumY += y; sumXY += x * y; sumXX += x * x; } const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); const eps = 0.02; // 趋势阈值(元/克/天) if (slope > eps) return '上涨'; if (slope < -eps) return '下跌'; return '持平'; } function computeChangeAmount(series, days) { if (series.length < days + 1) return { price7DaysAgo: null, change: null }; const last = series[series.length - 1]; const prev = series[series.length - 1 - days]; return { price7DaysAgo: Number(prev.toFixed(CONFIG.pricePrecisionDigits)), change: Number((last - prev).toFixed(CONFIG.pricePrecisionDigits)) }; } async function sendServerChan(title, desp, tags = '黄金价格') { const keys = SERVER_CHAN_KEYS; if (!keys || keys.length === 0) throw new Error('PUSH_KEYT 未配置'); const mod = await import('serverchan-sdk'); const scSend = mod.scSend || (mod.default && mod.default.scSend); if (!scSend) throw new Error('serverchan-sdk 未找到 scSend'); // 向所有key推送 const results = []; for (let i = 0; i < keys.length; i++) { const key = keys[i]; try { const response = await scSend(key, title, desp, { tags }); results.push({ keyIndex: i + 1, success: true, response }); console.log(`[gold] push sent to key ${i + 1}/${keys.length}. status:`, response); } catch (e) { results.push({ keyIndex: i + 1, success: false, error: e && e.message ? e.message : e }); console.log(`[gold] push failed for key ${i + 1}/${keys.length}:`, e && e.message ? e.message : e); } } // 如果所有key都失败,抛出错误 const allFailed = results.every(r => !r.success); if (allFailed) { throw new Error(`所有推送都失败: ${results.map(r => r.error || '未知错误').join('; ')}`); } return { statusCode: 200, body: JSON.stringify({ results, totalKeys: keys.length, successCount: results.filter(r => r.success).length }) }; } function formatNumber(n) { return Number(n).toFixed(CONFIG.pricePrecisionDigits); } function withinClosingWindow(ts = Date.now()) { const { hh, mm } = toCNTimeParts(ts); return hh === 22 && mm >= 30 && mm <= 59; } function upsertClosingPrice(data, price, ts = Date.now()) { const dateStr = toCNDate(ts); const existsIdx = data.closingPrices.findIndex(x => x.date === dateStr); if (existsIdx >= 0) { data.closingPrices[existsIdx].price = price; } else { data.closingPrices.push({ date: dateStr, price }); } while (data.closingPrices.length > 30) data.closingPrices.shift(); } function upsertOpeningPrice(data, price, ts = Date.now()) { const dateStr = toCNDate(ts); const existsIdx = data.openingPrices.findIndex(x => x.date === dateStr); if (existsIdx >= 0) { // 如果已存在,不更新(开盘价只记录一次) return false; } else { data.openingPrices.push({ date: dateStr, price }); while (data.openingPrices.length > 30) data.openingPrices.shift(); return true; } } function getYesterdayClosingPrice(data) { const now = Date.now(); const yesterdayTs = now - 24 * 60 * 60 * 1000; // 减去一天的毫秒数 const yesterdayStr = toCNDate(yesterdayTs); const yesterdayRecord = data.closingPrices.find(x => x.date === yesterdayStr); return yesterdayRecord ? yesterdayRecord.price : null; } function getTodayOpeningPrice(data) { const today = toCNDate(); const todayRecord = data.openingPrices.find(x => x.date === today); return todayRecord ? todayRecord.price : null; } function extractSeries(data) { return data.closingPrices.map(x => x.price); } function checkSpecialTargets(data, lastPrice, currentPrice) { const targets = Array.isArray(data.specialTargets) && data.specialTargets.length ? data.specialTargets : SPECIAL_PRICE_TARGETS; if (!targets || !targets.length) return null; // 如果没有上一笔价格,则退化为接近检测,避免首次运行完全不触发 if (lastPrice === undefined || lastPrice === null) { const tol = 0.2; // 首次检测仍保留一个很小的容差,建议范围:0.1-0.5 for (const t of targets) { if (Math.abs(currentPrice - t) <= tol) return t; } return null; } // 穿过监测:上一笔与当前价覆盖到目标价就算“穿过” // 说明:用 <= / >= 包含“刚好等于目标价”的边界,避免 prev===t 时离开 t 不触发的问题 for (const t of targets) { const prev = lastPrice; const curr = currentPrice; if ((prev <= t && curr >= t) || (prev >= t && curr <= t)) { return t; } } return null; } function buildPushPayload({ currentPrice, delta1h, change7, trend7, trend14, trend28, yesterdayClosingPrice, todayOpeningPrice, isSpecial, isTest, }) { const title = isSpecial ? `特殊提醒:黄金价格达到${formatNumber(currentPrice)}元/克` : (isTest ? '黄金价格监控(测试)' : '黄金价格监控'); const tags = isTest ? '黄金价格|TEST' : '黄金价格'; const parts = []; if (isSpecial) parts.push(`特殊提醒:黄金价格达到${formatNumber(currentPrice)}元/克;七日价格趋势:${trend7}`); parts.push(`- 当前黄金价格为:${formatNumber(currentPrice)}元/克`); parts.push(`- 一小时变化量为:${delta1h !== null ? formatNumber(delta1h) : '数据不足'}元/克`); parts.push(`- 七日前价格为:${change7.price7DaysAgo !== null ? formatNumber(change7.price7DaysAgo) : '数据不足'}元/克;七日价格变化量为:${change7.change !== null ? formatNumber(change7.change) : '数据不足'}元/克`); parts.push(`- 七日价格趋势为:${trend7}`); parts.push(`- 十四日价格趋势为:${trend14}`); parts.push(`- 二十八日价格趋势为:${trend28}`); parts.push(`- 昨日收盘价格为:${yesterdayClosingPrice !== null ? formatNumber(yesterdayClosingPrice) : '数据不足'}元/克`); parts.push(`- 今日开盘价格为:${todayOpeningPrice !== null ? formatNumber(todayOpeningPrice) : '数据不足'}元/克`); return { title, desp: parts.join(';\n'), tags }; } async function runCycle() { console.log('[gold] service starting...'); console.log('[gold] config:', { changeThresholdYuan: CONFIG.changeThresholdYuan, changeWindowMinutes: CONFIG.changeWindowMinutes, sampleIntervalMinutes: CONFIG.sampleIntervalMinutes, pricePrecisionDigits: CONFIG.pricePrecisionDigits, enableTimedPush: CONFIG.enableTimedPush, enableSpecialAlert: CONFIG.enableSpecialAlert, pushIntervalMinutes: CONFIG.pushIntervalMinutes, forcePushTest: CONFIG.forcePushTest, disabledWindows: CONFIG.disabledWindows, enableSpecialForcePush: CONFIG.enableSpecialForcePush, enableSpecialBypassInterval: CONFIG.enableSpecialBypassInterval, coolDownMinutesForSinglePrice: CONFIG.coolDownMinutesForSinglePrice, specialPriceCoolDownMinutes: CONFIG.specialPriceCoolDownMinutes, serverChanKeySource: SERVER_CHAN_KEY_DIRECT ? 'direct' : 'env', serverChanKeyCount: SERVER_CHAN_KEYS.length, }); const data = readData(); console.log('[gold] data loaded:', { priceHistoryCount: Array.isArray(data.priceHistory) ? data.priceHistory.length : 0, closingPricesCount: Array.isArray(data.closingPrices) ? data.closingPrices.length : 0, openingPricesCount: Array.isArray(data.openingPrices) ? data.openingPrices.length : 0, specialTargetsCount: Array.isArray(data.specialTargets) ? data.specialTargets.length : 0, }); const currentPrice = await fetchCurrentPrice(); const ts = nowTs(); // 记录今日开盘价格(如果是今天第一次运行) const openingPriceRecorded = upsertOpeningPrice(data, currentPrice, ts); if (openingPriceRecorded) { console.log('[gold] today opening price recorded:', currentPrice); } const last = data.priceHistory[data.priceHistory.length - 1]; if (!last || ts - last.ts >= CONFIG.sampleIntervalMinutes * 60 * 1000) { data.priceHistory.push({ ts, price: currentPrice }); while (data.priceHistory.length > 2000) data.priceHistory.shift(); console.log('[gold] appended to priceHistory. total:', data.priceHistory.length); } if (withinClosingWindow(ts)) { upsertClosingPrice(data, currentPrice, ts); console.log('[gold] within closing window. closing price upserted. total days:', data.closingPrices.length); } writeData(data); console.log('[gold] data saved to', DATA_FILE); const oneHourAgo = findDeltaWithin(data.priceHistory, CONFIG.changeWindowMinutes); const delta1h = oneHourAgo ? Number((currentPrice - oneHourAgo.price).toFixed(CONFIG.pricePrecisionDigits)) : null; console.log('[gold] compute deltas:', { windowMinutes: CONFIG.changeWindowMinutes, delta: delta1h }); const series = extractSeries(data); const change7 = computeChangeAmount(series, 7); const trend7 = series.length >= 7 ? computeTrendType(series.slice(-7)) : '数据不足'; const trend14 = series.length >= 14 ? computeTrendType(series.slice(-14)) : '数据不足'; const trend28 = series.length >= 28 ? computeTrendType(series.slice(-28)) : '数据不足'; console.log('[gold] trends:', { change7: change7.change, price7DaysAgo: change7.price7DaysAgo, trend7, trend14, trend28 }); // 获取昨日收盘价格和今日开盘价格 const yesterdayClosingPrice = getYesterdayClosingPrice(data); const todayOpeningPrice = getTodayOpeningPrice(data); console.log('[gold] daily prices:', { yesterdayClosingPrice, todayOpeningPrice }); const specialHit = checkSpecialTargets(data, last ? last.price : null, currentPrice); if (specialHit !== null) console.log('[gold] special target hit:', specialHit); // 检查特殊价格冷却期(每个价格单独的冷却时间) let isSpecialInCoolDown = false; if (specialHit !== null) { const now = nowTs(); const lastPushTsForThisPrice = data.lastSpecialPricePush[specialHit]; const coolDownMinutesForPrice = getSpecialPriceCoolDownMinutes(specialHit); if (coolDownMinutesForPrice > 0 && lastPushTsForThisPrice) { const elapsed = now - lastPushTsForThisPrice; const coolDownMs = coolDownMinutesForPrice * 60 * 1000; if (elapsed < coolDownMs) { isSpecialInCoolDown = true; const minutesPassed = Math.round(elapsed / 60000); console.log(`[gold] special price ${specialHit} in cool down: ${minutesPassed}/${coolDownMinutesForPrice} minutes`); } } } // 测试强制推送:无视触发条件与间隔限制,推送一次后退出 if (CONFIG.forcePushTest) { console.log('[gold] forcePushTest enabled: will push once and exit.'); const payload = buildPushPayload({ currentPrice, delta1h, change7, trend7, trend14, trend28, yesterdayClosingPrice, todayOpeningPrice, isSpecial: false, isTest: true, }); try { const resp = await sendServerChan(payload.title, payload.desp, payload.tags); console.log('[gold] [TEST] push sent. status:', resp && resp.statusCode); data.lastPushTs = nowTs(); writeData(data); } catch (e) { console.log('[gold] [TEST] push failed:', e && e.message ? e.message : e); } return; // 立即结束本次运行 } const shouldPushByDelta = CONFIG.enableTimedPush && delta1h !== null && Math.abs(delta1h) >= CONFIG.changeThresholdYuan; const shouldPushBySpecial = CONFIG.enableSpecialAlert && specialHit !== null && !isSpecialInCoolDown; const shouldPushByClosing = CONFIG.enableTimedPush && withinClosingWindow(ts); // 检查推送间隔限制 const now = nowTs(); // 普通推送(非特殊推送)使用独立的时间戳 const timeSinceLastNormalPush = now - data.lastNormalPushTs; const pushIntervalMs = CONFIG.pushIntervalMinutes * 60 * 1000; const canPushByInterval = timeSinceLastNormalPush >= pushIntervalMs; const inDisabledWindow = isInDisabledWindow(ts); const bypassDisabledBySpecial = shouldPushBySpecial && CONFIG.enableSpecialForcePush; const bypassIntervalBySpecial = shouldPushBySpecial && CONFIG.enableSpecialBypassInterval; console.log('[gold] push decisions:', { shouldPushByDelta, shouldPushBySpecial, shouldPushByClosing, canPushByInterval, inDisabledWindow, bypassDisabledBySpecial, bypassIntervalBySpecial, timeSinceLastNormalPushMinutes: Math.round(timeSinceLastNormalPush / 60000) }); const anyTrigger = shouldPushByDelta || shouldPushBySpecial || shouldPushByClosing; if (anyTrigger && (canPushByInterval || bypassIntervalBySpecial) && (!inDisabledWindow || bypassDisabledBySpecial)) { const payload = buildPushPayload({ currentPrice, delta1h, change7, trend7, trend14, trend28, yesterdayClosingPrice, todayOpeningPrice, isSpecial: shouldPushBySpecial, isTest: false, }); try { const resp = await sendServerChan(payload.title, payload.desp, payload.tags); console.log('[gold] push sent. status:', resp && resp.statusCode); // 更新最后推送时间:特殊推送不更新普通推送时间戳 data.lastPushTs = now; if (!shouldPushBySpecial || !CONFIG.enableSpecialBypassInterval) { data.lastNormalPushTs = now; } // 特殊推送时记录该价格的推送时间用于冷却期判断 if (shouldPushBySpecial && specialHit !== null) { data.lastSpecialPricePush[specialHit] = now; // 命中某个特殊价格时,可选地清零其他价格的冷却时间 if (CONFIG.resetOtherSpecialCooldownOnHit && data.lastSpecialPricePush && typeof data.lastSpecialPricePush === 'object') { const hitKey = String(specialHit); for (const k of Object.keys(data.lastSpecialPricePush)) { if (k !== hitKey) { // 归零表示立即失效,下次再次触发时可立即推送 delete data.lastSpecialPricePush[k]; } } } } writeData(data); } catch (e) { console.log('[gold] push failed:', e && e.message ? e.message : e); // 推送失败不影响数据持久化 } } else if (anyTrigger && !canPushByInterval && !bypassIntervalBySpecial) { console.log('[gold] push skipped by interval throttle:', { pushIntervalMinutes: CONFIG.pushIntervalMinutes, minutesSinceLast: Math.round(timeSinceLastNormalPush / 60000), specialBypassIntervalEnabled: CONFIG.enableSpecialBypassInterval, shouldPushBySpecial, }); } else if (anyTrigger && inDisabledWindow) { console.log('[gold] push skipped by disabled window:', { disabledWindows: CONFIG.disabledWindows, specialForcePushEnabled: CONFIG.enableSpecialForcePush, shouldPushBySpecial, }); } else if (!anyTrigger) { console.log('[gold] push skipped: no trigger matched', { delta1h, changeThresholdYuan: CONFIG.changeThresholdYuan, enableTimedPush: CONFIG.enableTimedPush, enableSpecialAlert: CONFIG.enableSpecialAlert, withinClosingWindow: withinClosingWindow(ts), }); } } let __isRunning = false; let __timer = null; function startScheduler() { console.log('[gold] scheduler starting: will run every', CONFIG.sampleIntervalMinutes, 'minute(s)'); const safeRun = async () => { if (__isRunning) { console.log('[gold] previous cycle still running, skip this tick.'); return; } __isRunning = true; try { await runCycle(); const now = nowTs(); if (withinClosingWindow(now)) { console.log('[gold] within closing window, stopping scheduler and exiting.'); if (__timer) clearInterval(__timer); process.exit(0); } } catch (e) { console.log('[gold] cycle error:', e && e.message ? e.message : e); } finally { __isRunning = false; } }; // run immediately once safeRun(); // then schedule __timer = setInterval(safeRun, CONFIG.sampleIntervalMinutes * 60 * 1000); } process.on('SIGINT', () => { console.log('[gold] SIGINT received, exiting.'); if (__timer) clearInterval(__timer); process.exit(0); }); process.on('SIGTERM', () => { console.log('[gold] SIGTERM received, exiting.'); if (__timer) clearInterval(__timer); process.exit(0); }); startScheduler();