diff --git a/README.md b/README.md index 9b0dd6e..0e3142d 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,4 @@ - 添加详细的代码注释。 - 优化协议检测逻辑:当目标主机 443 端口无法连接或超时时,自动降级并打印日志 `falling back to HTTP`,确保非 SSL 站点也能正常访问。 - 优化缓存逻辑:不再缓存小尺寸(< 15KB)的 GIF 图片(通常是 mShots 的 "Generating" 占位图),以便下次请求时能重新尝试获取真实截图。 + - 新增备用接口机制:当 mShots 返回无效图片(如生成中 GIF)或失败时,自动降级尝试使用 `thum.io` 获取截图,确保高可用性。 diff --git a/server.js b/server.js index da45a19..80dbc9d 100644 --- a/server.js +++ b/server.js @@ -9,6 +9,7 @@ require('dotenv').config(); const app = express(); const PORT = Number(process.env.PORT) || 11489; const UPSTREAM_HOST = 'https://s0.wp.com'; +const FALLBACK_HOST_BASE = 'https://image.thum.io/get/width/1024/crop/768/noanimate'; const CACHE_DIR = path.join(process.cwd(), 'cache'); fs.mkdirSync(CACHE_DIR, { recursive: true }); @@ -145,11 +146,37 @@ async function fetchUpstreamWithRetry(upstreamUrl, tries = 2) { } /** - * 核心处理逻辑:检查缓存 -> 回源 -> 写入缓存 -> 返回响应 + * 请求备用接口 (thum.io) + * @param {string} targetUrl + * @returns {Promise} + */ +async function fetchFallbackWithRetry(targetUrl) { + // thum.io 格式: https://image.thum.io/get// + const fallbackUrl = `${FALLBACK_HOST_BASE}/${targetUrl}`; + console.log(`[fallback-request] trying fallback: ${fallbackUrl}`); + + try { + return await axios.get(fallbackUrl, { + responseType: 'arraybuffer', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + }, + timeout: 30000, + validateStatus: () => true, + }); + } catch (err) { + console.error(`[fallback-error] msg=${err.message}`); + return null; + } +} + +/** + * 核心处理逻辑:检查缓存 -> 回源 -> (失败则) 备用接口 -> 写入缓存 -> 返回响应 * @param {object} res * @param {string} upstreamUrl + * @param {string} targetUrl */ -async function handleProxyRequest(res, upstreamUrl) { +async function handleProxyRequest(res, upstreamUrl, targetUrl) { const key = sha1(upstreamUrl); const { data: dataPath, meta: metaPath } = getCachePaths(key); @@ -176,27 +203,48 @@ async function handleProxyRequest(res, upstreamUrl) { // 2. 回源请求 const resp = await fetchUpstreamWithRetry(upstreamUrl); - // 3. 仅缓存有效图片 - if (isValidImageResponse(resp.status, resp.headers, resp.data)) { - const contentType = resp.headers['content-type'] || 'image/jpeg'; + // 3. 检查响应是否有效 + let finalResp = resp; + let isFallback = false; + + if (!isValidImageResponse(resp.status, resp.headers, resp.data)) { + console.log(`[upstream-invalid] url=${upstreamUrl} status=${resp.status} len=${resp.data.byteLength}, trying fallback...`); + const fallbackResp = await fetchFallbackWithRetry(targetUrl); + + if (fallbackResp && isValidImageResponse(fallbackResp.status, fallbackResp.headers, fallbackResp.data)) { + console.log(`[fallback-success] url=${targetUrl}`); + finalResp = fallbackResp; + isFallback = true; + } else { + console.log(`[fallback-failed] url=${targetUrl}, returning original response`); + } + } + + // 4. 仅缓存有效图片 (无论是回源还是备用) + if (isValidImageResponse(finalResp.status, finalResp.headers, finalResp.data)) { + const contentType = finalResp.headers['content-type'] || 'image/jpeg'; const meta = { - url: upstreamUrl, + url: isFallback ? `fallback:${targetUrl}` : upstreamUrl, contentType, - size: resp.data.byteLength, + size: finalResp.data.byteLength, createdAt: new Date().toISOString(), + source: isFallback ? 'thum.io' : 'mshots' }; try { - fs.writeFileSync(dataPath, resp.data); + fs.writeFileSync(dataPath, finalResp.data); fs.writeFileSync(metaPath, JSON.stringify(meta)); } catch (e) { // 写盘失败也继续返回 } res.type(contentType); res.set('Cache-Control', 'public, max-age=315360000, immutable'); - return res.send(resp.data); + if (isFallback) { + res.set('X-Source', 'fallback-thum.io'); + } + return res.send(finalResp.data); } - // 4. 非图片或非200:兜底策略 + // 5. 非图片或非200:兜底策略 // 若本地已有有效缓存(可能是旧的但有效),优先返回缓存 if (fs.existsSync(dataPath) && fs.existsSync(metaPath)) { try { @@ -263,7 +311,7 @@ app.use('/mshots/v1', async (req, res) => { // 拼接完整上游 URL const upstreamUrl = UPSTREAM_HOST + '/mshots/v1/' + targetUrl; - return handleProxyRequest(res, upstreamUrl); + return handleProxyRequest(res, upstreamUrl, targetUrl); }); // 根路径处理:/https://example.com 或 /www.baidu.com @@ -281,7 +329,7 @@ app.use(async (req, res) => { // 拼接完整上游 URL const upstreamUrl = UPSTREAM_HOST + '/mshots/v1/' + targetUrl; - return handleProxyRequest(res, upstreamUrl); + return handleProxyRequest(res, upstreamUrl, targetUrl); }); // 简单根路由健康检查