- monitor-server.js: Cookie存储改为Map合并机制,避免丢失CDN Cookie导致403 - checkActivity.js: 补上Cookie两步请求逻辑,修复无Cookie直接请求失败的问题 - public/index.html: 新增20秒刷新间隔选项
250 lines
9.3 KiB
JavaScript
250 lines
9.3 KiB
JavaScript
// checkActivity.js
|
||
const axios = require('axios');
|
||
const cheerio = require('cheerio');
|
||
const notifier = require('node-notifier');
|
||
|
||
const url = 'https://www.szhdy.com/activities/default.html?method=activity&id=9';
|
||
const checkInterval = 300000; // 5分钟检查一次
|
||
|
||
const headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||
'Connection': 'keep-alive',
|
||
'Upgrade-Insecure-Requests': '1',
|
||
};
|
||
|
||
// Cookie 管理
|
||
let cookieJar = new Map();
|
||
let cookieExpiry = 0;
|
||
|
||
function mergeCookiesFromHeaders(setCookieHeaders) {
|
||
for (const header of setCookieHeaders) {
|
||
const pair = header.split(';')[0];
|
||
const eqIdx = pair.indexOf('=');
|
||
if (eqIdx > 0) {
|
||
cookieJar.set(pair.substring(0, eqIdx).trim(), pair.substring(eqIdx + 1).trim());
|
||
}
|
||
}
|
||
}
|
||
|
||
function getCookieString() {
|
||
return Array.from(cookieJar.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
|
||
}
|
||
|
||
async function getValidCookies() {
|
||
if (cookieJar.size > 0 && Date.now() < cookieExpiry) {
|
||
return getCookieString();
|
||
}
|
||
console.log('[Cookie] 正在获取新的 Cookie...');
|
||
cookieJar.clear();
|
||
// 第一次请求拿 CDN Cookie(会返回 403,这是预期行为)
|
||
const res = await axios.get(url, {
|
||
headers,
|
||
maxRedirects: 0,
|
||
validateStatus: () => true, // 允许非 2xx 状态码
|
||
});
|
||
const setCookies = res.headers['set-cookie'] || [];
|
||
mergeCookiesFromHeaders(setCookies);
|
||
cookieExpiry = Date.now() + 3600 * 1000;
|
||
console.log('[Cookie] 获取成功:', getCookieString());
|
||
return getCookieString();
|
||
}
|
||
|
||
async function checkActivity() {
|
||
try {
|
||
const cookies = await getValidCookies();
|
||
const response = await axios.get(url, {
|
||
headers: { ...headers, Cookie: cookies },
|
||
});
|
||
const html = response.data;
|
||
|
||
// 如果响应内容过短,Cookie 可能失效,刷新重试
|
||
if (typeof html === 'string' && html.length < 1000) {
|
||
console.log('[Cookie] 响应内容过短,刷新 Cookie 重试...');
|
||
cookieJar.clear();
|
||
cookieExpiry = 0;
|
||
const retryCookies = await getValidCookies();
|
||
const retryRes = await axios.get(url, {
|
||
headers: { ...headers, Cookie: retryCookies },
|
||
});
|
||
mergeCookiesFromHeaders(retryRes.headers['set-cookie'] || []);
|
||
return parseAndCheck(retryRes.data);
|
||
}
|
||
|
||
// 合并新返回的 Cookie
|
||
mergeCookiesFromHeaders(response.headers['set-cookie'] || []);
|
||
return parseAndCheck(html);
|
||
} catch (error) {
|
||
console.error(`请求失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
function parseAndCheck(html) {
|
||
const $ = cheerio.load(html);
|
||
|
||
// 限定在活动主容器内检测,避免扫描其他区域
|
||
const $main = $('main.et-main.activities_bg');
|
||
if (!$main.length) {
|
||
console.error('活动主容器未找到,请检查页面结构');
|
||
return;
|
||
}
|
||
|
||
// 提取产品详情的工具函数
|
||
const extractAttribute = ($product, label) => {
|
||
const $container = $product.find('.form-container').filter((i, el) => {
|
||
const labelText = $(el).find('.form-title h5').text().trim();
|
||
// 同时匹配中文/英文冒号和空格差异
|
||
return labelText.replace(/:|:/g, '').includes(label.replace(/:|:/g, ''));
|
||
});
|
||
|
||
// 优先获取.form-text中的文本
|
||
let value = $container.find('.form-text').text().trim();
|
||
|
||
// 如果没有.form-text,尝试获取下拉选项中的文本
|
||
if (!value) {
|
||
value = $container.find('.act-dropdown-option').text().trim();
|
||
}
|
||
|
||
return value || '未知';
|
||
};
|
||
|
||
const extractProductDetails = (product) => {
|
||
const $product = $(product);
|
||
const name = $product.find('.cat-styleCtitle h1, .cat-styleBtitle h1').text().trim() || '未知服务器';
|
||
const core = extractAttribute($product, '核心:');
|
||
const memory = extractAttribute($product, '内存:');
|
||
const bandwidth = extractAttribute($product, '带宽:');
|
||
const duration = extractAttribute($product, '时长:');
|
||
|
||
// 同时提取价格和单位
|
||
const priceElement = $product.find('.main-price-current');
|
||
const priceUnitElement = $product.find('.price-current-unit');
|
||
const price = priceElement.text().trim() +
|
||
(priceUnitElement.length ? priceUnitElement.text().trim() : '');
|
||
|
||
return { name, core, memory, bandwidth, duration, price };
|
||
};
|
||
|
||
// 格式化产品列表(精确空行格式)
|
||
const formatProducts = (zoneTitle, products) => {
|
||
if (products.length === 0) return '';
|
||
|
||
const productLines = products.map(product => {
|
||
if ($(product).hasClass('cat-styleB-promotion-card') ||
|
||
$(product).hasClass('cat-styleC-promotion-card')) {
|
||
const details = extractProductDetails(product);
|
||
return `${details.name}\n${details.core} + ${details.memory}\n${details.bandwidth}\n${details.duration} + ${details.price}`;
|
||
}
|
||
return '未知产品';
|
||
}).join('\n\n'); // 产品之间空一行
|
||
|
||
// 严格按用户要求:
|
||
// - 专区标题前空两行
|
||
// - 专区标题后空一行
|
||
// - 产品列表前空一行
|
||
return `\n\n- [${zoneTitle}] 可购买产品:\n\n${productLines}`;
|
||
};
|
||
|
||
// 动态识别专区类型(解决ID变化问题)
|
||
const activityZones = $main.find('.activityContent_bg');
|
||
let valueZoneProducts = [];
|
||
let qualityZoneProducts = [];
|
||
let valueZoneSoldOut = [];
|
||
let qualityZoneSoldOut = [];
|
||
const debugInfo = [];
|
||
|
||
activityZones.each((i, zone) => {
|
||
const $zone = $(zone);
|
||
const title = $zone.find('.currency-title-a').text().trim();
|
||
|
||
// 支持两种产品结构:旧版(activity-row)和新版(cat-styleB/C-promotion-card)
|
||
const oldProducts = $zone.find('.activity-row a.list-product-content');
|
||
const newProducts = $zone.find('.cat-styleB-promotion-card, .cat-styleC-promotion-card');
|
||
|
||
// 处理旧版产品结构
|
||
const oldValidProducts = oldProducts.filter((i, el) => {
|
||
const href = $(el).attr('href');
|
||
const isNotAffiliate = !href.includes('fid=9');
|
||
const isNotSoldOut = !$(el).find('.list-product-desc').text().includes('已售');
|
||
return isNotAffiliate && isNotSoldOut;
|
||
});
|
||
|
||
const oldSoldOutProducts = oldProducts.filter((i, el) => {
|
||
const href = $(el).attr('href');
|
||
const isNotAffiliate = !href.includes('fid=9');
|
||
const isSoldOut = $(el).find('.list-product-desc').text().includes('已售');
|
||
return isNotAffiliate && isSoldOut;
|
||
});
|
||
|
||
// 处理新版产品结构
|
||
const newValidProducts = newProducts.filter((i, el) => {
|
||
return !$(el).find('.form-footer-butt').hasClass('disableButton');
|
||
});
|
||
|
||
const newSoldOutProducts = newProducts.filter((i, el) => {
|
||
return $(el).find('.form-footer-butt').hasClass('disableButton');
|
||
});
|
||
|
||
// 合并新旧产品
|
||
const validProducts = [...oldValidProducts, ...newValidProducts];
|
||
const soldOutProducts = [...oldSoldOutProducts, ...newSoldOutProducts];
|
||
|
||
debugInfo.push({
|
||
title,
|
||
total: oldProducts.length + newProducts.length,
|
||
valid: validProducts.length,
|
||
soldOut: soldOutProducts.length
|
||
});
|
||
|
||
if (title.includes('性价比专区')) {
|
||
valueZoneProducts = validProducts;
|
||
valueZoneSoldOut = soldOutProducts;
|
||
}
|
||
if (title.includes('质量型专区')) {
|
||
qualityZoneProducts = validProducts;
|
||
qualityZoneSoldOut = soldOutProducts;
|
||
}
|
||
});
|
||
|
||
// 输出调试信息
|
||
console.log(`\n检测到${activityZones.length}个活动专区:`);
|
||
debugInfo.forEach(info => {
|
||
console.log(`- [${info.title}] 总产品: ${info.total}, 可购买: ${info.valid}, 已售罄: ${info.soldOut}`);
|
||
});
|
||
|
||
// 状态判断逻辑
|
||
if (valueZoneProducts.length > 0 || qualityZoneProducts.length > 0) {
|
||
let productInfo = '';
|
||
|
||
// 生成性价比专区产品列表(严格按空行要求)
|
||
if (valueZoneProducts.length > 0) {
|
||
productInfo += formatProducts('性价比专区!预算党优选云服务器!', valueZoneProducts);
|
||
}
|
||
|
||
// 生成质量型专区产品列表(两个专区之间空两行)
|
||
if (qualityZoneProducts.length > 0) {
|
||
if (productInfo) productInfo += '\n\n';
|
||
productInfo += formatProducts('质量型专区!需求稳定用户优选!', qualityZoneProducts);
|
||
}
|
||
|
||
console.log(productInfo);
|
||
notifier.notify({
|
||
title: '活动产品上架提醒',
|
||
message: `监控的活动产品已经上架!${productInfo.replace(/\\n/g, '\n')}`,
|
||
sound: true,
|
||
wait: true
|
||
});
|
||
process.exit(0);
|
||
} else if (valueZoneSoldOut.length > 0 || qualityZoneSoldOut.length > 0) {
|
||
console.log('商品已售罄');
|
||
} else {
|
||
console.log(`[${new Date().toLocaleString()}] 暂未上架,继续监控...`);
|
||
}
|
||
}
|
||
|
||
// 立即执行一次
|
||
checkActivity();
|
||
|
||
// 定时执行
|
||
setInterval(checkActivity, checkInterval); |