const express = require('express'); const fetch = require('node-fetch'); const app = express(); const PORT = process.env.PORT || 8000; // 替换 Deno 的 Base64 编解码函数 function encodeBase64(data) { return Buffer.from(data).toString('base64'); } function decodeBase64(str) { return Buffer.from(str, 'base64'); } // 随机域名列表,与原脚本相同 const domains = [ 'https://luckiness-wea-df-demo.hf.space' ]; const CACHE_TTL = 5 * 60 * 1000; // 缓存有效期:5分钟 // 简单的内存缓存实现 class MemoryCache { constructor() { this.cache = new Map(); } get(key) { const item = this.cache.get(key); if (!item) return null; // 检查缓存是否过期 if (Date.now() - item.timestamp > CACHE_TTL) { this.cache.delete(key); return null; } return item.data; } set(key, data) { this.cache.set(key, { data, timestamp: Date.now() }); } // 清理过期缓存 cleanup() { const now = Date.now(); for (const [key, item] of this.cache.entries()) { if (now - item.timestamp > CACHE_TTL) { this.cache.delete(key); } } } } // 创建缓存实例 const cache = new MemoryCache(); // 定期清理缓存 setInterval(() => cache.cleanup(), CACHE_TTL); // 特定 user-agent,用于模拟手机浏览器访问 const iPhoneUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"; // PNG转WebP的URL转换 function urlPngToWebp(url) { if (url.includes("/webp/") && url.endsWith(".png")) return url.replace(/\.png$/, ".webp"); return url; } /** * 字符串转base64 * @param str */ function stringToBase64(str) { return Buffer.from(str).toString('base64'); } function getRandomAZ2() { const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; let result = ''; for (let i = 0; i < 2; i++) { const idx = Math.floor(Math.random() * chars.length); result += chars[idx]; } return result; } /** * 获取图片代理地址 * @param pathName * @param imageUrl */ const getProxyImageUrl = (pathName, imageUrl) => { pathName = domains[Math.floor(Math.random() * domains.length)]; const bfStr = stringToBase64(imageUrl); const str = getRandomAZ2() + bfStr; let url = `${pathName}/img/${str}`; return url; }; // 添加请求超时和重试功能的工具函数 async function fetchWithTimeout(url, options = {}, timeout = 10000, retries = 2) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); options.signal = controller.signal; let lastError; for (let attempt = 0; attempt <= retries; attempt++) { try { if (attempt > 0) { console.log(`[${new Date().toISOString()}] 重试请求 (${attempt}/${retries}): ${url}`); } const response = await fetch(url, options); clearTimeout(timeoutId); return response; } catch (error) { lastError = error; console.error(`[${new Date().toISOString()}] 请求失败 (${attempt}/${retries}): ${url}`, error.message); if (error.name === 'AbortError') { throw new Error(`请求超时 (${timeout}ms): ${url}`); } if (attempt === retries) { throw new Error(`请求失败 (已重试${retries}次): ${error.message}`); } // 等待一段时间后重试 await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); } } throw lastError; } // 处理天气数据 async function handleWeatherCnData(cacheKey, origin) { try { console.log(`[${new Date().toISOString()}] 获取天气数据`); const res = await fetchWithTimeout("https://m.weathercn.com/weatherMap.do?partner=1000001071_hfaw&language=zh-cn&id=2332685&p_source=&p_type=jump&seadId=&cpoikey=", { headers: { "User-Agent": iPhoneUserAgent, "Host": "m.weathercn.com", }, }, 15000, 3); // 检查响应状态 if (!res.ok) { throw new Error(`服务器返回错误状态: ${res.status} ${res.statusText}`); } const html = await res.text(); const match = html.match(/let\s+DATA\s*=\s*(\{[\s\S]+?\});/); if (!match) { throw new Error("未找到天气数据(DATA)"); } // 注意:在生产环境中谨慎使用 eval const DATA = new Function(`${match[0]}; return DATA;`)(); // ⚠️ 请确保来源可信 // 验证数据结构 if (!DATA || typeof DATA !== 'object') { throw new Error("无效的天气数据结构"); } // 处理图片 URL for (const key in DATA) { const item = DATA[key]; if (item.pic != null) { for (let i = 0; i < item.pic.length; i++) { item.pic[i] = getProxyImageUrl(origin, item.pic[i]); } } else if (item.result && item.result.picture_url) { for (let i = 0; i < item.result.picture_url.length; i++) { item.result.picture_url[i] = getProxyImageUrl(origin, item.result.picture_url[i]); } } } // 缓存数据 cache.set(cacheKey, { body: Buffer.from(JSON.stringify(DATA)), status: 200, headers: {'content-type': 'application/json'} }); return DATA; } catch (err) { console.error(`[${new Date().toISOString()}] 获取天气数据错误:`, err); throw err; } } // 处理短临降水数据 async function handleDuanlinData(pathname, cacheKey, origin) { try { const baseUrl = "https://img.weather.com.cn"; const target = baseUrl + pathname.replace("/duanlin", "/mpfv3"); const res = await fetchWithTimeout(target, { headers: { "User-Agent": iPhoneUserAgent, "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Host": "img.weather.com.cn", }, }, 15000, 3); // 检查响应状态 if (!res.ok) { throw new Error(`服务器返回错误状态: ${res.status} ${res.statusText}`); } const html = await res.text(); // 检查响应内容 if (!html || html.trim() === '' || html.indexOf('{') === -1) { console.error("Invalid response content:", html); throw new Error("无效的响应内容,未找到JSON数据"); } // 尝试解析JSON,增加错误处理 let data; try { const jsonStartIndex = html.indexOf('{'); const jsonContent = html.substring(jsonStartIndex); data = JSON.parse(jsonContent); } catch (jsonError) { console.error("JSON解析错误:", jsonError, "原始内容:", html); throw new Error(`JSON解析失败: ${jsonError.message}`); } // 验证必要的数据结构 if (!data.value || !Array.isArray(data.value) || data.value.length === 0) { throw new Error("数据结构不完整或为空"); } const imageUrl = baseUrl + "/mpfv3/"; const imageList = []; const times = []; for (let i = data.value.length - 1; i >= 0; i--) { const item = data.value[i]; // 验证数据项结构 if (!item.date || !Array.isArray(item.date) || item.date.length === 0 || !item.time || !Array.isArray(item.time) || !item.path || !Array.isArray(item.path)) { console.warn(`跳过无效数据项 #${i}:`, item); continue; } const time = item.date[0].substring(0, 8); // 修复类型错误 const reversedTimes = [...item.time].reverse(); const reversedPaths = [...item.path].reverse(); console.log('短临:'+origin) times.push(...reversedTimes.map(m => time + "" + m)); imageList.push(...reversedPaths.map(v => getProxyImageUrl(origin, imageUrl + v))); } // 验证是否有有效数据 if (times.length === 0 || imageList.length === 0) { throw new Error("未找到有效的时间或图片数据"); } // 安全地获取 stime 值 let stime = 0; if (data.stime && typeof data.stime === 'string') { stime = Number(data.stime.replace(/\D/g, "")) || 0; } const type = []; for (let s = 0; s < times.length; s++) { const time = Number(times[s].replace(/\D/g, "")); type.push(stime > time ? 1 : 2); } const dataParams = { rain_dl: { time: { obstime: data.obstime || "", stime: data.stime || "", }, pics_location_range: { bottom_lat: 10.160640206803123, left_lon: 73.44630749105424, top_lat: 53.560640206803123, right_lon: 135.09, }, result: { picture_url: imageList, forecast_time_list: times, type: type }, pic_type: "precipitation", }, }; // 缓存数据 cache.set(cacheKey, { body: Buffer.from(JSON.stringify(dataParams)), status: 200, headers: {'content-type': 'application/json'} }); return dataParams; } catch (error) { console.error("Error processing duanlin data:", error); // 返回一个空的有效数据结构,而不是抛出错误 return { rain_dl: { time: { obstime: "", stime: "", }, pics_location_range: { bottom_lat: 10.160640206803123, left_lon: 73.44630749105424, top_lat: 53.560640206803123, right_lon: 135.09, }, result: { picture_url: [], forecast_time_list: [], type: [] }, pic_type: "precipitation", error: error.message }, }; } } // 处理图片代理 async function handleImageProxy(pathname, cacheKey) { try { const encodedUrl = pathname.replace("/img/", ""); let decodedUrl = null; // 1. 先尝试 decodeURIComponent try { const url = decodeURIComponent(encodedUrl); if (url.startsWith("http://") || url.startsWith("https://")) { decodedUrl = url; } } catch (error) { console.log(`[${new Date().toISOString()}] URL解码失败: ${error.message}`); } // 2. 如果不是合法 URL,再尝试 Base64 解码 if (!decodedUrl) { try { let base64Decoded = decodeBase64(encodedUrl.substring(2)).toString(); console.log(`[${new Date().toISOString()}] Base64解码第一次: ${base64Decoded}`); if (base64Decoded.startsWith("https://luckiness")) { const two = base64Decoded.split("/img/")[1].substring(2); base64Decoded = decodeBase64(two).toString(); console.log(`[${new Date().toISOString()}] Base64解码第二次: ${base64Decoded}`); } if ( base64Decoded.startsWith("http://") || base64Decoded.startsWith("https://") ) { decodedUrl = base64Decoded; } } catch (error) { console.log(`[${new Date().toISOString()}] Base64解码失败: ${error.message}`); } } if (!decodedUrl) { throw new Error("无效的图片URL"); } console.log(`[${new Date().toISOString()}] 处理图片: ${decodedUrl}`); decodedUrl = urlPngToWebp(decodedUrl); const res = await fetchWithTimeout(decodedUrl, { headers: { "User-Agent": iPhoneUserAgent, "accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer": new URL(decodedUrl).origin }, }, 15000, 2); // 检查响应状态 if (!res.ok) { throw new Error(`图片服务器返回错误状态: ${res.status} ${res.statusText}`); } const contentType = res.headers.get("content-type") || "image/png"; const body = await res.arrayBuffer(); const imageBuffer = Buffer.from(body); // 缓存响应 cache.set(cacheKey, { body: imageBuffer, status: res.status, headers: { 'content-type': contentType, 'cache-control': 'public, max-age=86400' // 客户端缓存1天 } }); return { buffer: imageBuffer, contentType, status: res.status }; } catch (error) { console.error(`[${new Date().toISOString()}] 图片代理错误:`, error); throw error; } } // 路由处理 app.use(express.json()); app.use(express.raw({ limit: '50mb', type: () => true })); // 中间件:处理所有请求的通用逻辑 app.use((req, res, next) => { const pathname = req.path + (req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''); const cacheKey = `${req.method}:${pathname}`; // 检查缓存(仅对GET请求) if (req.method === "GET") { const cachedResponse = cache.get(cacheKey); if (cachedResponse) { res.status(cachedResponse.status); for (const [key, value] of Object.entries(cachedResponse.headers)) { if (Array.isArray(value)) { res.set(key, value[0]); } else { res.set(key, value); } } return res.send(cachedResponse.body); } } // 如果没有缓存命中,继续处理请求 next(); }); // 健康检查路由 app.get('/health', (req, res) => { console.log(`[${new Date().toISOString()}] 健康检查请求`); res.status(200).json({ status: 'ok', uptime: process.uptime(), timestamp: Date.now() }); }); // 根路由 - 服务状态页面 app.get('/', (req, res) => { const html = `
服务状态: 运行中
启动时间: ${new Date(Date.now() - process.uptime() * 1000).toLocaleString()}