Compare commits
12 Commits
219e930062
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b222d40ce6 | ||
|
|
af935a1ea2 | ||
|
|
fb47fbbb2a | ||
|
|
72e032980b | ||
|
|
245625ae7d | ||
|
|
ad8bbc715e | ||
| f9fccd5661 | |||
| 6af236fd78 | |||
| 271a3ddc64 | |||
|
|
939269b1c5 | ||
|
|
d6ddc3232a | ||
|
|
766e6678b7 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
gold_data.json
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
579
GoldPrice.js
Normal file
579
GoldPrice.js
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
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();
|
||||||
143
README.md
143
README.md
@@ -1,2 +1,143 @@
|
|||||||
# goldworm_js
|
# 工商银行黄金价格监控与推送脚本
|
||||||
|
|
||||||
|
这是一个基于 Node.js 的自动化脚本,用于定时监控中国工商银行(ICBC)官网的黄金递延(T+D)价格,并通过 ServerChan(方糖服务)将价格提醒推送至微信。脚本支持价格突破报警、多维度趋势分析、防骚扰冷却机制以及收盘价记录等功能。
|
||||||
|
|
||||||
|
## ✨ 核心功能
|
||||||
|
|
||||||
|
* **定时抓取**:自动请求工商银行黄金价格页面,并使用 `axios + cheerio` 解析实时价格(无需浏览器)。
|
||||||
|
* **智能推送**:
|
||||||
|
* **变化触发**:当价格在设定时间窗口内波动超过阈值时推送。
|
||||||
|
* **特殊价位**:可设置一组目标价位,当价格“穿过”任一目标价时立即推送特殊提醒。
|
||||||
|
* **收盘推送**:在每日收盘时段(可配置)自动记录并推送收盘价。
|
||||||
|
* **趋势分析**:自动计算并报告一小时变化量、七日变化量,以及七日、十四日、二十八日的价格趋势(上涨/下跌/持平)。
|
||||||
|
* **灵活配置**:所有运行参数(如采样间隔、价格阈值、禁用时段、冷却时间等)均在脚本内 `CONFIG` 对象中集中管理,易于修改。
|
||||||
|
* **数据持久化**:自动将价格历史、收盘价、开盘价及推送状态保存至本地 JSON 文件。
|
||||||
|
* **多Key推送**:支持配置多个 ServerChan SendKey,向多个微信终端同时推送消息。
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 获取项目
|
||||||
|
```bash
|
||||||
|
git clone http://43.135.34.133:53000/LiuEnder/goldworm_js
|
||||||
|
cd gold-price-monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装项目依赖
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
此步骤将安装 `axios`、`cheerio` 和 `serverchan-sdk`。
|
||||||
|
|
||||||
|
### 3. 配置环境变量
|
||||||
|
脚本运行需要一个关键环境变量:
|
||||||
|
|
||||||
|
1. **ServerChan 推送密钥 (`PUSH_KEYT`)**:
|
||||||
|
* 前往 https://sc3.ft07.com/ 登录并获取您的 `SendKey`。
|
||||||
|
* **设置环境变量**(以下为Linux/macOS示例,永久生效请添加到 `~/.bashrc` 或 `~/.zshrc`):
|
||||||
|
```bash
|
||||||
|
export PUSH_KEYT="SCTxxxxxx...你的SendKey"
|
||||||
|
# 如果需要配置多个Key,用分号(;)分隔
|
||||||
|
# export PUSH_KEYT="Key1;Key2;Key3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 运行脚本
|
||||||
|
```bash
|
||||||
|
# 直接运行一次(测试用)
|
||||||
|
node GoldPriceV6cooldown.js
|
||||||
|
|
||||||
|
# 或使用 npm 脚本
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
首次运行会创建数据文件 `gold_data.json` 并开始监控。检查控制台输出以确认运行成功。
|
||||||
|
|
||||||
|
## ⚙️ 详细配置
|
||||||
|
|
||||||
|
脚本的主要行为由文件顶部的 `CONFIG` 对象控制。您可以根据需要修改:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const CONFIG = {
|
||||||
|
changeThresholdYuan: 1.0, // 触发普通推送的1小时价格变化阈值(元/克)
|
||||||
|
changeWindowMinutes: 60, // 计算变化量的时间窗口(分钟)
|
||||||
|
sampleIntervalMinutes: 2, // 抓取价格的采样间隔(分钟)
|
||||||
|
pricePrecisionDigits: 2, // 价格显示和计算的小数位数
|
||||||
|
enableTimedPush: true, // 是否启用定时推送(变化触发和收盘推送)
|
||||||
|
enableSpecialAlert: true, // 是否启用特殊价格提醒
|
||||||
|
pushIntervalMinutes: 60, // 普通推送的最小间隔(分钟),防骚扰
|
||||||
|
forcePushTest: false, // 设为 true 可测试推送功能,运行一次即退出
|
||||||
|
disabledWindows: [12001500], // 禁止推送的时间段,格式 [开始时间结束时间],如 12001500 表示 12:00-15:00
|
||||||
|
enableSpecialForcePush: true, // 特殊价格提醒是否忽略禁用时间段
|
||||||
|
enableSpecialBypassInterval: true, // 特殊价格提醒是否忽略推送间隔限制
|
||||||
|
coolDownMinutesForSinglePrice: 90, // 单个特殊价位的默认冷却时间(分钟)
|
||||||
|
resetOtherSpecialCooldownOnHit: true, // 触发一个特殊价位时,是否重置其他价位的冷却
|
||||||
|
// 为特定价位设置独立的冷却时间(分钟),未配置的价位使用默认值
|
||||||
|
specialPriceCoolDownMinutes: {
|
||||||
|
// “600”: 120, // 例如,当价格达到600元时,冷却时间设为120分钟
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**特殊价格目标设置**:
|
||||||
|
在 `CONFIG` 对象下方,找到 `SPECIAL_PRICE_TARGETS` 数组,将您关心的价位填入。
|
||||||
|
```javascript
|
||||||
|
const SPECIAL_PRICE_TARGETS = [500, 520, 550]; // 例如,设置500、520、550元为目标价位
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏃 生产环境部署
|
||||||
|
|
||||||
|
建议使用进程管理工具(如 **PM2**)来保证脚本持续运行。
|
||||||
|
|
||||||
|
1. **全局安装 PM2**:
|
||||||
|
```bash
|
||||||
|
npm install -g pm2
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **使用 PM2 启动服务**(确保已在项目目录中):
|
||||||
|
```bash
|
||||||
|
# 启动并命名进程
|
||||||
|
pm2 start GoldPriceV6cooldown.js --name gold-monitor
|
||||||
|
# 设置开机自启(根据PM2提示操作)
|
||||||
|
pm2 startup
|
||||||
|
pm2 save
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **常用 PM2 命令**:
|
||||||
|
```bash
|
||||||
|
pm2 logs gold-monitor # 查看实时日志
|
||||||
|
pm2 status # 查看所有进程状态
|
||||||
|
pm2 stop gold-monitor # 停止服务
|
||||||
|
pm2 restart gold-monitor # 重启服务
|
||||||
|
pm2 monit # 打开监控面板
|
||||||
|
```
|
||||||
|
|
||||||
|
**通过 Crontab 或青龙面板调度**:
|
||||||
|
如果您希望脚本在特定时间运行(而非持续后台运行),可以配置定时任务。例如,每天在交易时间运行:
|
||||||
|
```bash
|
||||||
|
# 每天9点到22点,每30分钟运行一次
|
||||||
|
*/30 9-22 * * * cd /path/to/your/project && node GoldPriceV6cooldown.js >> /tmp/gold_monitor.log 2>&1
|
||||||
|
```
|
||||||
|
请注意,脚本内部已包含定时循环逻辑 (`sampleIntervalMinutes`),如果通过外部Cron调用,通常只需调用一次,脚本会自行循环并在收盘时间退出。
|
||||||
|
|
||||||
|
## 📁 文件说明
|
||||||
|
|
||||||
|
- `GoldPriceV6cooldown.js` - 主脚本文件
|
||||||
|
- `gold_data.json` - 运行时自动生成的数据文件(记录价格历史、推送状态等)
|
||||||
|
- `package.json` - 项目依赖定义
|
||||||
|
- `.gitignore` - Git忽略文件配置
|
||||||
|
- `README.md` - 本说明文件
|
||||||
|
|
||||||
|
## ⚠️ 重要注意事项
|
||||||
|
|
||||||
|
1. **合规使用**:本工具仅用于个人学习与技术研究。请合理设置请求频率,避免对工商银行服务器造成不必要的压力。使用请遵守网站相关规定。
|
||||||
|
2. **选择器更新**:如果目标网页结构改版,可能需要更新脚本中的 `PRICE_SELECTOR` 变量。您可以使用浏览器的开发者工具检查元素。
|
||||||
|
3. **推送频率限制**:免费版 ServerChan 有推送次数限制,请合理配置 `pushIntervalMinutes` 和 `coolDownMinutesForSinglePrice` 等参数。
|
||||||
|
4. **环境变量安全**:务必通过环境变量设置 `PUSH_KEYT`,切勿将其直接写入代码或提交至版本库。
|
||||||
|
|
||||||
|
## 🔧 故障排查
|
||||||
|
|
||||||
|
- **抓取失败/解析失败**:确认网络可访问目标页面;如果网页结构改版,检查并更新脚本中的 `PRICE_SELECTOR`。
|
||||||
|
- **无推送**:检查 `PUSH_KEYT` 环境变量是否设置正确;查看控制台日志确认价格是否成功抓取及触发条件是否满足。
|
||||||
|
- **推送过于频繁**:调整 `CONFIG.pushIntervalMinutes` 和 `CONFIG.changeThresholdYuan` 参数。
|
||||||
|
|
||||||
|
## 📄 开源协议
|
||||||
|
|
||||||
|
本项目基于 GPLv3 协议开源。
|
||||||
6
changelog.md
Normal file
6
changelog.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 规划功能
|
||||||
|
- 重点关注价格特殊推送
|
||||||
|
- 变化率计算
|
||||||
|
# 8.0.0
|
||||||
|
修正推送范围的小bug
|
||||||
|
从puppeteer换到axios
|
||||||
618
package-lock.json
generated
Normal file
618
package-lock.json
generated
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
{
|
||||||
|
"name": "gold-price-monitor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "gold-price-monitor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
|
"npm-check-updates": "^19.6.3",
|
||||||
|
"serverchan-sdk": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.6",
|
||||||
|
"resolved": "https://r.cnpmjs.org/axios/-/axios-1.13.6.tgz",
|
||||||
|
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.11",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://r.cnpmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cheerio": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/cheerio/-/cheerio-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cheerio-select": "^2.1.0",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"encoding-sniffer": "^0.2.1",
|
||||||
|
"htmlparser2": "^10.1.0",
|
||||||
|
"parse5": "^7.3.0",
|
||||||
|
"parse5-htmlparser2-tree-adapter": "^7.1.0",
|
||||||
|
"parse5-parser-stream": "^7.1.2",
|
||||||
|
"undici": "^7.19.0",
|
||||||
|
"whatwg-mimetype": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cheerio-select": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cross-fetch": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://r.cnpmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://r.cnpmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://r.cnpmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://r.cnpmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/encoding-sniffer": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://r.cnpmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://r.cnpmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://r.cnpmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://r.cnpmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://r.cnpmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"entities": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://r.cnpmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/npm-check-updates": {
|
||||||
|
"version": "19.6.3",
|
||||||
|
"resolved": "https://r.cnpmjs.org/npm-check-updates/-/npm-check-updates-19.6.3.tgz",
|
||||||
|
"integrity": "sha512-VAt9Bp26eLaymZ0nZyh5n/by+YZIuegXlvWR0yv1zBqd984f8VnEnBbn+1lS3nN5LyEjn62BJ+yYgzNSpb6Gzg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"ncu": "build/cli.js",
|
||||||
|
"npm-check-updates": "build/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0",
|
||||||
|
"npm": ">=8.12.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"parse5": "^7.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5-parser-stream": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://r.cnpmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||||
|
"dependencies": {
|
||||||
|
"parse5": "^7.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5/node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
|
"node_modules/serverchan-sdk": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://r.cnpmjs.org/serverchan-sdk/-/serverchan-sdk-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-0vSdwwaCYaHsQZloQ2004JeN+ydkwNK/u0tnH5y7pCbavsyX0cQfZyaQfN1AjjemVNK6lZjNifefNUCzoCFnfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-fetch": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "7.22.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/undici/-/undici-7.22.0.tgz",
|
||||||
|
"integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://r.cnpmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://r.cnpmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://r2.cnpmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
package.json
Normal file
15
package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "gold-price-monitor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A script to monitor ICBC gold price and push notifications via ServerChan.",
|
||||||
|
"main": "GoldPrice.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node GoldPrice.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
|
"npm-check-updates": "^19.6.3",
|
||||||
|
"serverchan-sdk": "^1.0.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
471
testpage.html
Normal file
471
testpage.html
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
<!-- 使用说明
|
||||||
|
保存文件:将上面的代码保存为 gold_test.html文件。
|
||||||
|
启动本地服务器:
|
||||||
|
# 在包含 gold_test.html 的目录中运行
|
||||||
|
python3 -m http.server 8000
|
||||||
|
或使用其他HTTP服务器。
|
||||||
|
访问测试页面:在浏览器中打开 http://localhost:8000/gold_test.html
|
||||||
|
修改监控脚本配置:
|
||||||
|
在您的 GoldPrice.js脚本中,修改以下配置:
|
||||||
|
// 将原来的工商银行URL替换为测试页面地址
|
||||||
|
const TARGET_URL = 'http://localhost:8000/gold_test.html';
|
||||||
|
|
||||||
|
// 价格选择器保持不变(与测试页面一致)
|
||||||
|
const PRICE_SELECTOR = '#activeprice_080020000521'; -->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>黄金价格测试页面</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(to right, #d4af37, #b8951e);
|
||||||
|
color: white;
|
||||||
|
padding: 25px 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-display {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-price {
|
||||||
|
font-size: 52px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #d4af37;
|
||||||
|
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-controls {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group input:focus {
|
||||||
|
border-color: #d4af37;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-btn {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-btn:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.increase-btn {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.increase-btn:hover {
|
||||||
|
background-color: #0069d9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.decrease-btn {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decrease-btn:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-history {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-history h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
list-style-type: none;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list li {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-time {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-price {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border-left: 5px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions h3 {
|
||||||
|
color: #0066cc;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions p {
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions code {
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-price {
|
||||||
|
font-size: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-change {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positive {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neutral {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>黄金价格测试页面</h1>
|
||||||
|
<p>用于测试黄金价格监控脚本的模拟页面</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="price-display">
|
||||||
|
<div class="price-label">当前黄金价格 (元/克)</div>
|
||||||
|
<div id="activeprice_080020000521" class="current-price">1150.22</div>
|
||||||
|
<div id="priceChange" class="price-change neutral">价格未变化</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="price-controls">
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="priceInput">设置新价格 (元/克)</label>
|
||||||
|
<input type="number" id="priceInput" step="0.01" min="0" value="1150.22" placeholder="输入新的黄金价格">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button id="setPriceBtn" class="set-btn">设置价格</button>
|
||||||
|
<button id="increaseBtn" class="increase-btn">+5 元</button>
|
||||||
|
<button id="decreaseBtn" class="decrease-btn">-5 元</button>
|
||||||
|
<button id="resetBtn" class="reset-btn">重置为 1150.22</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="price-history">
|
||||||
|
<h3>价格历史记录</h3>
|
||||||
|
<ul id="historyList" class="history-list">
|
||||||
|
<li>
|
||||||
|
<span class="history-time">刚刚</span>
|
||||||
|
<span class="history-price">1150.22 元/克</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="history-time">1分钟前</span>
|
||||||
|
<span class="history-price">1149.53 元/克</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instructions">
|
||||||
|
<h3>使用说明</h3>
|
||||||
|
<p>1. 此页面模拟了黄金价格页面,价格元素的选择器为 <code>#activeprice_080020000521</code>,与您的监控脚本使用的选择器一致。</p>
|
||||||
|
<p>2. 通过上方输入框和按钮可以修改页面显示的黄金价格。</p>
|
||||||
|
<p>3. 您可以在本地运行此页面,然后将监控脚本中的 <code>TARGET_URL</code> 修改为此页面的地址(如 <code>http://localhost:8000/test.html</code>)进行测试。</p>
|
||||||
|
<p>4. 您可以使用 Python 的简单HTTP服务器运行此页面:<code>python3 -m http.server 8000</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 获取DOM元素
|
||||||
|
const priceElement = document.getElementById('activeprice_080020000521');
|
||||||
|
const priceInput = document.getElementById('priceInput');
|
||||||
|
const setPriceBtn = document.getElementById('setPriceBtn');
|
||||||
|
const increaseBtn = document.getElementById('increaseBtn');
|
||||||
|
const decreaseBtn = document.getElementById('decreaseBtn');
|
||||||
|
const resetBtn = document.getElementById('resetBtn');
|
||||||
|
const historyList = document.getElementById('historyList');
|
||||||
|
const priceChangeElement = document.getElementById('priceChange');
|
||||||
|
|
||||||
|
// 价格历史记录
|
||||||
|
let priceHistory = [
|
||||||
|
{ price: 1150.22, time: '刚刚' },
|
||||||
|
{ price: 1149.53, time: '1分钟前' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 上一个价格,用于计算变化
|
||||||
|
let previousPrice = 1150.22;
|
||||||
|
|
||||||
|
// 更新时间显示
|
||||||
|
function formatTime() {
|
||||||
|
const now = new Date();
|
||||||
|
return now.toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新价格变化显示
|
||||||
|
function updatePriceChange(currentPrice) {
|
||||||
|
const change = currentPrice - previousPrice;
|
||||||
|
const changePercent = ((change / previousPrice) * 100).toFixed(2);
|
||||||
|
|
||||||
|
if (change > 0) {
|
||||||
|
priceChangeElement.textContent = `+${change.toFixed(2)} 元 (+${changePercent}%)`;
|
||||||
|
priceChangeElement.className = 'price-change positive';
|
||||||
|
} else if (change < 0) {
|
||||||
|
priceChangeElement.textContent = `${change.toFixed(2)} 元 (${changePercent}%)`;
|
||||||
|
priceChangeElement.className = 'price-change negative';
|
||||||
|
} else {
|
||||||
|
priceChangeElement.textContent = '价格未变化';
|
||||||
|
priceChangeElement.className = 'price-change neutral';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新历史记录
|
||||||
|
function updateHistory(newPrice) {
|
||||||
|
// 将当前价格添加到历史记录顶部
|
||||||
|
priceHistory.unshift({
|
||||||
|
price: newPrice,
|
||||||
|
time: formatTime()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只保留最近的10条记录
|
||||||
|
if (priceHistory.length > 10) {
|
||||||
|
priceHistory.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新历史记录显示
|
||||||
|
historyList.innerHTML = '';
|
||||||
|
priceHistory.forEach(item => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.innerHTML = `
|
||||||
|
<span class="history-time">${item.time}</span>
|
||||||
|
<span class="history-price">${item.price.toFixed(2)} 元/克</span>
|
||||||
|
`;
|
||||||
|
historyList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新价格
|
||||||
|
function setNewPrice() {
|
||||||
|
const newPrice = parseFloat(priceInput.value);
|
||||||
|
|
||||||
|
if (isNaN(newPrice) || newPrice <= 0) {
|
||||||
|
alert('请输入有效的正数价格');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新价格变化显示
|
||||||
|
updatePriceChange(newPrice);
|
||||||
|
|
||||||
|
// 更新页面显示的价格
|
||||||
|
priceElement.textContent = newPrice.toFixed(2);
|
||||||
|
|
||||||
|
// 更新历史记录
|
||||||
|
updateHistory(newPrice);
|
||||||
|
|
||||||
|
// 更新输入框值为新价格
|
||||||
|
priceInput.value = newPrice.toFixed(2);
|
||||||
|
|
||||||
|
// 记录当前价格作为下一次的上一个价格
|
||||||
|
previousPrice = newPrice;
|
||||||
|
|
||||||
|
console.log(`价格已更新为: ${newPrice.toFixed(2)} 元/克`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加价格
|
||||||
|
function increasePrice() {
|
||||||
|
const currentPrice = parseFloat(priceElement.textContent);
|
||||||
|
const newPrice = currentPrice + 5;
|
||||||
|
priceInput.value = newPrice.toFixed(2);
|
||||||
|
setNewPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减少价格
|
||||||
|
function decreasePrice() {
|
||||||
|
const currentPrice = parseFloat(priceElement.textContent);
|
||||||
|
const newPrice = Math.max(0, currentPrice - 5);
|
||||||
|
priceInput.value = newPrice.toFixed(2);
|
||||||
|
setNewPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置价格
|
||||||
|
function resetPrice() {
|
||||||
|
priceInput.value = '1150.22';
|
||||||
|
setNewPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加事件监听器
|
||||||
|
setPriceBtn.addEventListener('click', setNewPrice);
|
||||||
|
increaseBtn.addEventListener('click', increasePrice);
|
||||||
|
decreaseBtn.addEventListener('click', decreasePrice);
|
||||||
|
resetBtn.addEventListener('click', resetPrice);
|
||||||
|
|
||||||
|
// 允许通过按Enter键设置价格
|
||||||
|
priceInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setNewPrice();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化价格变化显示
|
||||||
|
updatePriceChange(1150.22);
|
||||||
|
|
||||||
|
// 初始控制台提示
|
||||||
|
console.log('黄金价格测试页面已加载');
|
||||||
|
console.log('当前价格元素选择器: #activeprice_080020000521');
|
||||||
|
console.log('当前价格: 1150.22 元/克');
|
||||||
|
console.log('您可以通过页面上的控件修改价格,然后使用监控脚本抓取测试。');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user