Spaces:
Paused
Paused
| 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 = ` | |
| <!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>demo服务</title> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| line-height: 1.6; | |
| } | |
| h1 { | |
| color: #0066cc; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 10px; | |
| } | |
| .status { | |
| background: #f8f9fa; | |
| border-radius: 4px; | |
| padding: 15px; | |
| margin: 20px 0; | |
| } | |
| .status-ok { | |
| color: #28a745; | |
| font-weight: bold; | |
| } | |
| .endpoints { | |
| margin-top: 30px; | |
| } | |
| .endpoint { | |
| margin-bottom: 10px; | |
| padding: 10px; | |
| border-left: 3px solid #0066cc; | |
| background: #f8f9fa; | |
| } | |
| code { | |
| background: #eee; | |
| padding: 2px 4px; | |
| border-radius: 3px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>demo服务</h1> | |
| <div class="status"> | |
| <p>服务状态: <span class="status-ok">运行中</span></p> | |
| <p>启动时间: ${new Date(Date.now() - process.uptime() * 1000).toLocaleString()}</p> | |
| </div> | |
| </body> | |
| </html> | |
| `; | |
| res.send(html); | |
| }); | |
| // 1. 获取并解析天气数据 | |
| app.get('/weathercn-data/', async (req, res) => { | |
| try { | |
| console.log(`[${new Date().toISOString()}] 处理天气数据请求`); | |
| const cacheKey = req.url; | |
| const cachedData = cache.get(cacheKey); | |
| if (cachedData) { | |
| console.log(`[${new Date().toISOString()}] 使用缓存的天气数据`); | |
| res.status(cachedData.status); | |
| for (const [key, value] of Object.entries(cachedData.headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(cachedData.body); | |
| } | |
| const origin = req.headers.origin || `${req.protocol}://${req.get('host')}`; | |
| const data = await handleWeatherCnData(cacheKey, origin); | |
| if (data) { | |
| return res.json(data); | |
| } else { | |
| throw new Error("处理天气数据失败"); | |
| } | |
| } catch (error) { | |
| console.error(`[${new Date().toISOString()}] 天气数据请求错误:`, error); | |
| return res.status(500).json({ | |
| error: "处理天气数据时发生错误", | |
| message: error.message | |
| }); | |
| } | |
| }); | |
| // 2. 代理请求:/mpf/* | |
| app.all('/mpf/*', async (req, res) => { | |
| try { | |
| console.log(`[${new Date().toISOString()}] 处理MPF请求: ${req.path}`); | |
| const pathname = req.path; | |
| const cacheKey = req.url; | |
| const cachedData = cache.get(cacheKey); | |
| if (cachedData) { | |
| console.log(`[${new Date().toISOString()}] 使用缓存的MPF数据: ${cacheKey}`); | |
| res.status(cachedData.status); | |
| for (const [key, value] of Object.entries(cachedData.headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(cachedData.body); | |
| } | |
| const target = `https://mpf.weather.com.cn${pathname.replace('/mpf', '')}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`; | |
| const response = await fetchWithTimeout(target, { | |
| method: req.method, | |
| headers: { | |
| 'User-Agent': iPhoneUserAgent, | |
| 'Referer': 'https://m.weather.com.cn/', | |
| 'Host': 'mpf.weather.com.cn' | |
| }, | |
| body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined | |
| }, 15000, 2); | |
| const status = response.status; | |
| const headers = {}; | |
| response.headers.forEach((value, key) => { | |
| if (!['content-encoding', 'content-length'].includes(key)) { | |
| headers[key] = value; | |
| } | |
| }); | |
| const body = await response.arrayBuffer(); | |
| const buffer = Buffer.from(body); | |
| if (req.method === "GET" && status === 200) { | |
| cache.set(cacheKey, { body: buffer, status, headers }); | |
| } | |
| res.status(status); | |
| for (const [key, value] of Object.entries(headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(buffer); | |
| } catch (error) { | |
| console.error(`[${new Date().toISOString()}] MPF请求错误:`, error); | |
| return res.status(500).json({ | |
| error: "处理MPF数据时发生错误", | |
| message: error.message | |
| }); | |
| } | |
| }); | |
| // 3. 特别处理 /duanlin/* 返回加工后的降水图数据 | |
| app.get("/duanlin/*", async (req, res) => { | |
| try { | |
| console.log(`[${new Date().toISOString()}] 处理短临降水请求: ${req.path}`); | |
| const cacheKey = req.url; | |
| const cachedData = cache.get(cacheKey); | |
| if (cachedData) { | |
| console.log(`[${new Date().toISOString()}] 使用缓存的短临降水数据: ${cacheKey}`); | |
| res.status(cachedData.status); | |
| for (const [key, value] of Object.entries(cachedData.headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(cachedData.body); | |
| } | |
| const origin = req.headers.origin || `${req.protocol}://${req.get('host')}`; | |
| const data = await handleDuanlinData(req.path, cacheKey, origin); | |
| if (data && data.rain_dl) { | |
| if (data.rain_dl.error) { | |
| console.warn(`[${new Date().toISOString()}] 短临降水数据处理警告: ${data.rain_dl.error}`); | |
| } | |
| return res.json(data); | |
| } else { | |
| throw new Error("处理短临降水数据失败"); | |
| } | |
| } catch (error) { | |
| console.error(`[${new Date().toISOString()}] 短临降水数据处理错误:`, error); | |
| return res.status(500).json({ | |
| error: "处理短临降水数据时发生错误", | |
| message: error.message | |
| }); | |
| } | |
| }); | |
| // 4. 小米代理 | |
| app.get('/wtr-v3/*', async (req, res) => { | |
| try { | |
| console.log(`[${new Date().toISOString()}] 处理小米天气请求: ${req.path}`); | |
| const pathname = req.path; | |
| const cacheKey = req.url; | |
| const cachedData = cache.get(cacheKey); | |
| if (cachedData) { | |
| console.log(`[${new Date().toISOString()}] 使用缓存的小米天气数据: ${cacheKey}`); | |
| res.status(cachedData.status); | |
| for (const [key, value] of Object.entries(cachedData.headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(cachedData.body); | |
| } | |
| const target = `https://weatherapi.market.xiaomi.com${pathname}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`; | |
| const response = await fetchWithTimeout(target, { | |
| headers: { | |
| 'User-Agent': iPhoneUserAgent | |
| } | |
| }, 15000, 2); | |
| const status = response.status; | |
| const headers = {}; | |
| response.headers.forEach((value, key) => { | |
| if (!['content-encoding', 'content-length'].includes(key)) { | |
| headers[key] = value; | |
| } | |
| }); | |
| const body = await response.arrayBuffer(); | |
| const buffer = Buffer.from(body); | |
| if (status === 200) { | |
| cache.set(cacheKey, { body: buffer, status, headers }); | |
| } | |
| res.status(status); | |
| for (const [key, value] of Object.entries(headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(buffer); | |
| } catch (error) { | |
| console.error(`[${new Date().toISOString()}] 小米天气请求错误:`, error); | |
| return res.status(500).json({ | |
| error: "处理小米天气数据时发生错误", | |
| message: error.message | |
| }); | |
| } | |
| }); | |
| // 5. 图片代理 /img/* | |
| app.get('/img/*', async (req, res) => { | |
| try { | |
| console.log(`[${new Date().toISOString()}] 处理图片代理请求: ${req.path}`); | |
| const pathname = req.path; | |
| const cacheKey = req.url; | |
| const cachedData = cache.get(cacheKey); | |
| if (cachedData) { | |
| console.log(`[${new Date().toISOString()}] 使用缓存的图片: ${cacheKey}`); | |
| res.status(cachedData.status); | |
| for (const [key, value] of Object.entries(cachedData.headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(cachedData.buffer||cachedData.body); | |
| } | |
| const target = pathname.replace('/img/', ''); | |
| const response = await handleImageProxy(pathname, cacheKey); | |
| const status = response.status; | |
| const headers = {contentType:response.contentType}; | |
| const buffer =response.buffer; | |
| res.status(status); | |
| for (const [key, value] of Object.entries(headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(buffer); | |
| } catch (error) { | |
| console.error(`[${new Date().toISOString()}] 图片代理请求错误:`, error); | |
| return res.status(500).json({ | |
| error: "处理图片代理请求时发生错误", | |
| message: error.message | |
| }); | |
| } | |
| }); | |
| // 6. d3代理 | |
| app.get('/d3/*', async (req, res) => { | |
| try { | |
| console.log(`[${new Date().toISOString()}] 处理D3天气请求: ${req.path}`); | |
| const pathname = req.path; | |
| const cacheKey = req.url; | |
| const cachedData = cache.get(cacheKey); | |
| if (cachedData) { | |
| console.log(`[${new Date().toISOString()}] 使用缓存的D3天气数据: ${cacheKey}`); | |
| res.status(cachedData.status); | |
| for (const [key, value] of Object.entries(cachedData.headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(cachedData.body); | |
| } | |
| //https://d3.weather.com.cn 证书过期了先用http | |
| const target = `http://d3.weather.com.cn${pathname.replace('/d3', '')}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`; | |
| const response = await fetchWithTimeout(target, { | |
| headers: { | |
| 'User-Agent': iPhoneUserAgent, | |
| 'Referer': 'http://m.weather.com.cn/' //'https://m.weather.com.cn/' | |
| } | |
| }, 15000, 2); | |
| const status = response.status; | |
| const headers = {}; | |
| response.headers.forEach((value, key) => { | |
| if (!['content-encoding', 'content-length'].includes(key)) { | |
| headers[key] = value; | |
| } | |
| }); | |
| const body = await response.arrayBuffer(); | |
| const buffer = Buffer.from(body); | |
| if (status === 200) { | |
| cache.set(cacheKey, { body: buffer, status, headers }); | |
| } | |
| res.status(status); | |
| for (const [key, value] of Object.entries(headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(buffer); | |
| } catch (error) { | |
| console.error(`[${new Date().toISOString()}] D3天气请求错误:`, error); | |
| return res.status(500).json({ | |
| error: "处理D3天气数据时发生错误", | |
| message: error.message | |
| }); | |
| } | |
| }); | |
| // 7. d4代理 | |
| app.get('/d4/*', async (req, res) => { | |
| try { | |
| console.log(`[${new Date().toISOString()}] 处理D4天气请求: ${req.path}`); | |
| const pathname = req.path; | |
| const cacheKey = req.url; | |
| const cachedData = cache.get(cacheKey); | |
| if (cachedData) { | |
| console.log(`[${new Date().toISOString()}] 使用缓存的D4天气数据: ${cacheKey}`); | |
| res.status(cachedData.status); | |
| for (const [key, value] of Object.entries(cachedData.headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(cachedData.body); | |
| } | |
| const target = `https://d4.weather.com.cn${pathname.replace('/d4', '')}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`; | |
| const response = await fetchWithTimeout(target, { | |
| headers: { | |
| 'User-Agent': iPhoneUserAgent, | |
| 'Referer': 'https://m.weather.com.cn/' | |
| } | |
| }, 15000, 2); | |
| const status = response.status; | |
| const headers = {}; | |
| response.headers.forEach((value, key) => { | |
| if (!['content-encoding', 'content-length'].includes(key)) { | |
| headers[key] = value; | |
| } | |
| }); | |
| const body = await response.arrayBuffer(); | |
| const buffer = Buffer.from(body); | |
| if (status === 200) { | |
| cache.set(cacheKey, { body: buffer, status, headers }); | |
| } | |
| res.status(status); | |
| for (const [key, value] of Object.entries(headers)) { | |
| res.setHeader(key, value); | |
| } | |
| return res.send(buffer); | |
| } catch (error) { | |
| console.error(`[${new Date().toISOString()}] D4天气请求错误:`, error); | |
| return res.status(500).json({ | |
| error: "处理D4天气数据时发生错误", | |
| message: error.message | |
| }); | |
| } | |
| }); | |
| // 添加全局错误处理中间件 | |
| app.use((err, req, res, next) => { | |
| console.error(`[${new Date().toISOString()}] 全局错误: ${err.message}`); | |
| console.error(err.stack); | |
| res.status(500).json({ | |
| error: '服务器内部错误', | |
| message: err.message | |
| }); | |
| }); | |
| // 404处理 | |
| app.use((req, res) => { | |
| console.log(`[${new Date().toISOString()}] 404 未找到: ${req.originalUrl}`); | |
| res.status(404).json({ error: 'Not Found' }); | |
| }); | |
| // 启动服务器 | |
| app.listen(PORT, () => { | |
| console.log(`[${new Date().toISOString()}] 服务器运行在端口 ${PORT}`); | |
| }); | |
| module.exports = app; // 为了在 Hugging Face 部署可能需要导出 app | |