Files
hdy/checkActivity.js
ikmkj 7907716c9f fix: Cookie管理优化及刷新间隔补充
- monitor-server.js: Cookie存储改为Map合并机制,避免丢失CDN Cookie导致403
- checkActivity.js: 补上Cookie两步请求逻辑,修复无Cookie直接请求失败的问题
- public/index.html: 新增20秒刷新间隔选项
2026-03-19 15:21:01 +08:00

250 lines
9.3 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.
// 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);