Files
gold-worm-icbc/GoldPriceV6cooldown.js
2026-03-09 19:46:30 +08:00

557 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//我需要一个定时获取浏览器中黄金价格并推送到微信的程序
//网站网址https://mybank.icbc.com.cn/icbc/newperbank/perbank3/gold/goldaccrual_query_out.jsp
//黄金价格在页面中,需要获取到黄金价格并推送到微信
//黄金价格的selector为#activeprice_080020000521
//黄金价格的单位为:元/克
//黄金价格的更新时间为每半小时检测一次变化量小于1元/克则不推送。
//根据你的判断使用合适的算法计算价格趋势。
//根据你的判断使用外部json储存或其他方式储存数据。
//一个数组记录三十天的收盘价格用于计算七日价格变化量七天价格趋势十四天价格趋势二十八天价格趋势趋势类型1.上涨 2.下跌 3.持平。
//推送格式为当前黄金价格为xx元/克一小时变化量为xx元/克七日价格变化量为xx元/克七日价格趋势为xx十四日价格趋势为xx二十八日价格趋势为xx。
//该脚本需要使用NodeJS编写并使用Puppeteer库来获取页面内容。同时留出一个数组用作特殊价格时的设置当黄金价格达到该价格时推送提醒。数组格式为[价格1,价格2,价格3,价格4,价格5,价格6,价格7]
//特殊提醒格式为特殊提醒黄金价格达到xx元/克七日价格趋势xx。
//需要使用ServerChan来推送信息ServerChan的sendkey使用环境变量变量名为PUSH_KEYT
//程序每天由crontab定时启动启动时间由外部的青龙面板设置设置为每天晚上2230后关闭。
// 此下为信息推送用程序例子
// import {scSend} from 'serverchan-sdk';
// const response = await scSend('sendkey', 'title', 'desp', { tags: '黄金价格' });
// console.log('Response:', response);
const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer-core');
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');
// 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 = [];
// 可配置项:变化量阈值(元/克)、变化计算窗口(分钟)、采样间隔(分钟)、价格精度(小数位数)
const CONFIG = {
changeThresholdYuan: 1.0,
changeWindowMinutes: 60,
sampleIntervalMinutes: 2,
pricePrecisionDigits: 2,
enableTimedPush: true, // 定时推送开关(变化量触发和收盘推送)
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] launching browser to fetch price...');
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
executablePath: process.env.CHROME_PATH || '/usr/bin/chromium-browser' // 默认路径,可通过环境变量覆盖
});
try {
const page = await browser.newPage();
await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector(PRICE_SELECTOR, { timeout: 60000 });
const text = await page.$eval(PRICE_SELECTOR, el => el.textContent || el.innerText || '');
const numeric = parseFloat(String(text).replace(/[^0-9.\-]/g, ''));
if (!isFinite(numeric)) throw new Error('无法解析价格');
console.log(`[gold] fetched current price: ${numeric}`);
return numeric;
} finally {
await browser.close();
}
}
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; // 首次检测仍保留一个很小的容差
for (const t of targets) {
if (Math.abs(currentPrice - t) <= tol) return t;
}
return null;
}
// 穿过监测:上一笔和当前价格在目标价两侧,就认为“穿过”该价格
for (const t of targets) {
const prev = lastPrice;
const curr = currentPrice;
if ((prev < t && curr >= t) || (prev > t && curr <= t)) {
return t;
}
}
return null;
}
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 title = `黄金价格监控(测试)`;
const parts = [];
parts.push(`[TEST] 当前黄金价格为:${formatNumber(currentPrice)}元/克`);
const oneHourAgoForTest = findDeltaWithin(data.priceHistory, CONFIG.changeWindowMinutes);
const delta1hForTest = oneHourAgoForTest ? Number((currentPrice - oneHourAgoForTest.price).toFixed(CONFIG.pricePrecisionDigits)) : null;
parts.push(`[TEST] 一小时变化量为:${delta1hForTest !== null ? formatNumber(delta1hForTest) : '数据不足'}元/克`);
const seriesForTest = extractSeries(data);
const change7ForTest = computeChangeAmount(seriesForTest, 7);
const trend7ForTest = seriesForTest.length >= 7 ? computeTrendType(seriesForTest.slice(-7)) : '数据不足';
const trend14ForTest = seriesForTest.length >= 14 ? computeTrendType(seriesForTest.slice(-14)) : '数据不足';
const trend28ForTest = seriesForTest.length >= 28 ? computeTrendType(seriesForTest.slice(-28)) : '数据不足';
parts.push(`[TEST] 七日前价格为:${change7ForTest.price7DaysAgo !== null ? formatNumber(change7ForTest.price7DaysAgo) : '数据不足'}元/克`);
parts.push(`[TEST] 七日价格变化量为:${change7ForTest.change !== null ? formatNumber(change7ForTest.change) : '数据不足'}元/克`);
parts.push(`[TEST] 七日价格趋势为:${trend7ForTest}`);
parts.push(`[TEST] 十四日价格趋势为:${trend14ForTest}`);
parts.push(`[TEST] 二十八日价格趋势为:${trend28ForTest}`);
const yesterdayClosingPriceForTest = getYesterdayClosingPrice(data);
const todayOpeningPriceForTest = getTodayOpeningPrice(data);
parts.push(`[TEST] 昨日收盘价格为:${yesterdayClosingPriceForTest !== null ? formatNumber(yesterdayClosingPriceForTest) : '数据不足'}元/克`);
parts.push(`[TEST] 今日开盘价格为:${todayOpeningPriceForTest !== null ? formatNumber(todayOpeningPriceForTest) : '数据不足'}元/克`);
const desp = parts.join(';\n');
try {
const resp = await sendServerChan(title, desp, '黄金价格|TEST');
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 title = shouldPushBySpecial ? `特殊提醒:黄金价格达到${formatNumber(currentPrice)}元/克` : `黄金价格监控`;
const parts = [];
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) : '数据不足'}元/克`);
if (shouldPushBySpecial) parts.unshift(`特殊提醒:黄金价格达到${formatNumber(currentPrice)}元/克;七日价格趋势:${trend7}`);
const desp = parts.join(';\n');
try {
const resp = await sendServerChan(title, desp, '黄金价格');
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();