3764 words
19 minutes
用Cloudflare Workers搭建Wikipedia镜像指南
当知识自由成为奢望,技术便是破局的利刃。
Workers免费限额
Worker免费套餐允许:
- 每日10万次请求
- 单请求CPU最多50ms
- 全局KV存储1GB
通常情况下少数人使用是完全够的,但是毕竟限额摆在那,不建议直接公开使用
具体步骤
- 进入
CloudFlare Workers
(https://workers.cloudflare.com/) - 点击
创建服务
- 修改
服务名称
这个将会决定到你网站的域名,并记住你设置的网址 - 最后点击
创建服务
来创建 Workers - 在接下来的网页中点击
快速编辑
,进入编辑界面 - 在左侧代码框中,把所有代码删掉,替换成以下我贴出的代码
- 点击
保存并部署
,就完成了
代码我已经写好了,如果有更改,我会更新
版本信息Ver 1.1
上次更新 2025/02/29
版本说明分为密码版和公共版
公共版没有密码,密码版需要配置
PROXY_PASSWORD
环境变量来设置密码
公共版
// 配置选项const CONFIG = { TARGET_HOST: 'zh.wikipedia.org', ALLOWED_METHODS: new Set(['GET', 'HEAD', 'POST', 'OPTIONS']), MAX_BODY_SIZE: 10 * 1024 * 1024, // 10MB TIMEOUT: 30000, // 30秒 ALLOWED_CONTENT_TYPES: new Set(['application/json', 'text/plain', 'text/html']), CACHE_TTL: 3600, // 响应缓存时间 1小时 // 跳过的请求头 SKIP_HEADERS_PREFIX: new Set([ 'cf-', 'cdn-loop', 'x-forwarded-', 'x-real-ip', 'connection', 'keep-alive', 'upgrade', 'transfer-encoding', ]), // 安全响应头 SECURITY_HEADERS: { 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'SAMEORIGIN', 'X-XSS-Protection': '1; mode=block', 'Referrer-Policy': 'strict-origin-when-cross-origin' }, // 预编译正则表达式以提高性能 CONTENT_TYPE_REGEX: /^(application\/json|text\/plain|text\/html)(;|$)/}
// 错误处理类class ProxyError extends Error { constructor(message, status = 500) { super(message); this.name = 'ProxyError'; this.status = status; }}
// 使用单一事件监听器处理所有请求addEventListener('fetch', event => { event.respondWith( handleRequest(event.request) .catch(handleError) );});
/** * 主请求处理函数 * @param {Request} request 原始请求 * @returns {Promise<Response>} 响应 */async function handleRequest(request) { // 验证请求方法 - 快速失败原则 if (!CONFIG.ALLOWED_METHODS.has(request.method)) { throw new ProxyError('Method not allowed', 405); }
// 处理POST请求的特殊验证 if (request.method === 'POST') { const contentType = request.headers.get('Content-Type');
// 验证Content-Type - 使用预编译的正则表达式 if (contentType && !CONFIG.CONTENT_TYPE_REGEX.test(contentType)) { throw new ProxyError('Unsupported Content-Type', 415); }
// 验证请求大小 const contentLength = request.headers.get('content-length'); if (contentLength && parseInt(contentLength, 10) > CONFIG.MAX_BODY_SIZE) { throw new ProxyError('Request entity too large', 413); } }
// 构建目标URL - 避免重复解析 const url = new URL(request.url); const targetUrl = `https://${CONFIG.TARGET_HOST}${url.pathname}${url.search}`;
// 准备并发送请求 const proxyRequest = await createProxyRequest(request, targetUrl); const response = await fetchWithTimeout(proxyRequest, CONFIG.TIMEOUT);
// 返回处理后的响应 return createProxyResponse(response);}
/** * 创建代理请求 - 优化头部处理 * @param {Request} originalRequest 原始请求 * @param {string} targetUrl 目标URL */async function createProxyRequest(originalRequest, targetUrl) { // 直接利用Headers API进行头部处理 const headers = new Headers(); const skipPrefixCheck = shouldSkipHeader;
// 使用迭代器一次性处理所有头部 for (const [key, value] of originalRequest.headers) { if (!skipPrefixCheck(key)) { headers.set(key, value); } }
// 设置必要的请求头 headers.set('Host', CONFIG.TARGET_HOST); headers.set('Origin', `https://${CONFIG.TARGET_HOST}`); headers.set('Referer', `https://${CONFIG.TARGET_HOST}`); headers.set('X-Forwarded-Proto', 'https');
// 只在需要时获取CF连接IP const cfIp = originalRequest.headers.get('cf-connecting-ip'); if (cfIp) headers.set('X-Real-IP', cfIp);
// 利用条件判断优化body处理 const needsBody = !['GET', 'HEAD'].includes(originalRequest.method);
// 构建新请求 return new Request(targetUrl, { method: originalRequest.method, headers, body: needsBody ? originalRequest.body : undefined, redirect: 'follow', });}
/** * 创建代理响应 * @param {Response} originalResponse 原始响应 * @returns {Response} 修改后的响应 */function createProxyResponse(originalResponse) { const headers = new Headers(); const skipPrefixCheck = shouldSkipHeader;
// 一次性处理所有头部 for (const [key, value] of originalResponse.headers) { if (!skipPrefixCheck(key)) { headers.set(key, value); } }
// 批量设置安全头 Object.entries(CONFIG.SECURITY_HEADERS).forEach(([key, value]) => { headers.set(key, value); });
// 设置缓存控制 headers.set('Cache-Control', `public, max-age=${CONFIG.CACHE_TTL}`);
// 创建新响应 return new Response(originalResponse.body, { status: originalResponse.status, statusText: originalResponse.statusText, headers });}
/** * 优化的头部跳过检查函数 * @param {string} headerName 头部名称 * @returns {boolean} 是否需要跳过 */function shouldSkipHeader(headerName) { const lowerName = headerName.toLowerCase();
// 使用Set的has方法直接检查前缀开头 for (const prefix of CONFIG.SKIP_HEADERS_PREFIX) { if (lowerName.startsWith(prefix)) { return true; } } return false;}
/** * 带超时的fetch * @param {Request} request 请求 * @param {number} timeout 超时时间(ms) * @returns {Promise<Response>} 响应 */async function fetchWithTimeout(request, timeout) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout);
try { const response = await fetch(request, { signal: controller.signal }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new ProxyError('Request timeout', 504); } throw error; }}
/** * 错误处理函数 * @param {Error} error 错误对象 * @returns {Response} 错误响应 */function handleError(error) { const status = error instanceof ProxyError ? error.status : 500; const message = error.message || 'Internal Server Error';
// 预定义错误响应头以减少对象创建 const errorHeaders = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' };
// 记录错误 console.error(`[ProxyError] ${status}: ${message}`, error.stack);
return new Response( JSON.stringify({ error: message, status }), { status, headers: errorHeaders } );}
密码版
IMPORTANT需要设置环境变量
PROXY_PASSWORD
来设置密码
// 配置选项const CONFIG = { TARGET_HOST: 'zh.wikipedia.org', ALLOWED_METHODS: new Set(['GET', 'HEAD', 'POST', 'OPTIONS']), MAX_BODY_SIZE: 10 * 1024 * 1024, // 10MB TIMEOUT: 30000, // 30秒 // 更新允许的内容类型,添加图片和其他资源类型 ALLOWED_CONTENT_TYPES: new Set([ 'application/json', 'text/plain', 'text/html', 'image/', 'font/', 'application/javascript', 'text/css', 'application/xml', 'application/pdf' ]), CACHE_TTL: 3600, // 响应缓存时间 1小时 // 图片等静态资源的缓存时间更长 STATIC_CACHE_TTL: 86400, // 24小时 // 跳过的请求头 SKIP_HEADERS_PREFIX: new Set([ 'cf-', 'cdn-loop', 'x-forwarded-', 'x-real-ip', 'connection', 'keep-alive', 'upgrade', 'transfer-encoding', ]), // 安全响应头 - 修改CSP以允许图片和其他资源 SECURITY_HEADERS: { 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'SAMEORIGIN', 'X-XSS-Protection': '1; mode=block', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Content-Security-Policy': "default-src 'self' https://*.wikimedia.org https://*.wikipedia.org; img-src 'self' https://*.wikimedia.org https://*.wikipedia.org data:; style-src 'self' 'unsafe-inline' https://*.wikimedia.org https://*.wikipedia.org; font-src 'self' https://*.wikimedia.org https://*.wikipedia.org; script-src 'self' 'unsafe-inline' https://*.wikimedia.org https://*.wikipedia.org; frame-ancestors 'none'" }, // 内容类型检查 - 改为函数以支持更灵活的判断 isAllowedContentType: (contentType) => { if (!contentType) return false; const ct = contentType.toLowerCase(); for (const allowedType of CONFIG.ALLOWED_CONTENT_TYPES) { if (ct.startsWith(allowedType)) return true; } return false; }, // 身份验证配置 AUTH: { COOKIE_NAME: 'auth_token', TOKEN_TTL: 7 * 24 * 60 * 60, // 7天(秒) TOKEN_SECRET_LENGTH: 32 // 密钥长度 }};
// 错误处理类class ProxyError extends Error { constructor(message, status = 500) { super(message); this.name = 'ProxyError'; this.status = status; }}
// 主导出函数export default { async fetch(request, env, ctx) { try { // 验证必要的环境变量 if (!env.PROXY_PASSWORD) { throw new ProxyError('Missing required environment variable: PROXY_PASSWORD', 500); }
return await handleRequestWithAuth(request, env); } catch (error) { return handleError(error); } }};
/** * 身份验证和请求处理的入口 * @param {Request} request 原始请求 * @param {Object} env 环境变量 * @returns {Promise<Response>} 响应 */async function handleRequestWithAuth(request, env) { const url = new URL(request.url);
// 为登录页面提供静态资源 if (url.pathname === '/login.css' || url.pathname === '/login.js') { return handleStaticResource(url.pathname); }
// 处理登录请求 if (url.pathname === '/login' && request.method === 'POST') { return handleLogin(request, env); }
// 检查cookie中的令牌 const token = getCookieValue(request, CONFIG.AUTH.COOKIE_NAME);
// 如果没有令牌或令牌无效,展示登录页面 if (!token || !await isValidToken(token, env)) { return showLoginPage(); }
// 令牌有效,处理正常请求 return handleRequest(request, env);}
/** * 处理登录请求 * @param {Request} request 登录请求 * @param {Object} env 环境变量 * @returns {Promise<Response>} 登录响应 */async function handleLogin(request, env) { // 确保是POST请求 if (request.method !== 'POST') { throw new ProxyError('Method not allowed', 405); }
// 验证内容类型 const contentType = request.headers.get('Content-Type'); if (!contentType || !contentType.includes('application/x-www-form-urlencoded')) { throw new ProxyError('Unsupported Content-Type', 415); }
let formData; try { formData = await request.formData(); } catch { throw new ProxyError('Invalid form data', 400); }
// 修复: 正确处理 formData.get('password') 返回的值,可能是 string 或 File const passwordValue = formData.get('password'); // 确保密码是字符串 const password = typeof passwordValue === 'string' ? passwordValue : '';
if (!password) { throw new ProxyError('Password is required', 400); }
// 安全地验证密码 - 使用恒定时间比较避免计时攻击 if (await secureCompare(password, env.PROXY_PASSWORD)) { // 生成安全令牌 const token = await generateSecureToken(env);
// 创建响应并设置cookie const headers = new Headers({ 'Location': '/', 'Set-Cookie': `${CONFIG.AUTH.COOKIE_NAME}=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${CONFIG.AUTH.TOKEN_TTL}` });
return new Response(null, { status: 302, headers }); } else { // 使用延时以防止计时攻击 await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 500));
// 记录失败的登录尝试 console.warn(`Failed login attempt from IP: ${request.headers.get('cf-connecting-ip')}`);
// 密码错误,显示登录页面并附带错误信息 return showLoginPage('密码不正确,请重试。'); }}
/** * 恒定时间比较函数 - 防止计时攻击 * @param {string} a 字符串1 * @param {string} b 字符串2 * @returns {Promise<boolean>} 比较结果 */async function secureCompare(a, b) { if (typeof a !== 'string' || typeof b !== 'string') { return false; }
// 如果长度不同,返回false但耗时相同 const aLen = a.length; const bLen = b.length; const maxLen = Math.max(aLen, bLen);
let result = aLen === bLen ? 1 : 0;
for (let i = 0; i < maxLen; i++) { // 对超出范围的索引使用默认值0,确保恒定时间 const aChar = i < aLen ? a.charCodeAt(i) : 0; const bChar = i < bLen ? b.charCodeAt(i) : 0; result &= aChar === bChar ? 1 : 0; }
return result === 1;}
/** * 生成安全令牌 * @param {Object} env 环境变量 * @returns {Promise<string>} 安全令牌 */async function generateSecureToken(env) { // 创建随机数组 const randomBytes = new Uint8Array(CONFIG.AUTH.TOKEN_SECRET_LENGTH); crypto.getRandomValues(randomBytes);
// 转换为base64,放入令牌 const randomStr = btoa(String.fromCharCode(...randomBytes)); const timestamp = Date.now().toString(36);
// 使用随机字符串和时间戳创建令牌 const tokenData = `${timestamp}.${randomStr}`;
// 创建签名以验证令牌 const signature = await createTokenSignature(tokenData, env.PROXY_PASSWORD);
return `${tokenData}.${signature}`;}
/** * 为令牌创建签名 * @param {string} tokenData 令牌数据 * @param {string} secret 密钥 * @returns {Promise<string>} 签名 */async function createTokenSignature(tokenData, secret) { // 使用SubtleCrypto进行HMAC签名 const encoder = new TextEncoder(); const keyData = encoder.encode(secret); const messageData = encoder.encode(tokenData);
// 从密码创建密钥 const key = await crypto.subtle.importKey( 'raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] );
// 创建签名 const signature = await crypto.subtle.sign('HMAC', key, messageData);
// 转换为base64 return btoa(String.fromCharCode(...new Uint8Array(signature)));}
/** * 验证令牌是否有效 * @param {string} token 身份验证令牌 * @param {Object} env 环境变量 * @returns {Promise<boolean>} 是否有效 */async function isValidToken(token, env) { try { // 从令牌中提取时间戳和签名 const parts = token.split('.'); if (parts.length !== 3) return false;
const [timestamp, randomStr, signature] = parts; const tokenData = `${timestamp}.${randomStr}`;
// 验证签名 const expectedSignature = await createTokenSignature(tokenData, env.PROXY_PASSWORD); if (!await secureCompare(signature, expectedSignature)) { return false; }
// 验证时间戳 const tokenTime = parseInt(timestamp, 36); const now = Date.now();
// 检查令牌是否过期 return (now - tokenTime) < (CONFIG.AUTH.TOKEN_TTL * 1000); } catch { return false; }}
/** * 从请求中获取cookie值 * @param {Request} request 请求 * @param {string} name cookie名称 * @returns {string|null} cookie值或null */function getCookieValue(request, name) { const cookieHeader = request.headers.get('Cookie') || ''; const cookies = cookieHeader.split(';').map(c => c.trim());
for (const cookie of cookies) { const [cookieName, ...valueParts] = cookie.split('='); if (cookieName === name) { // 使用join以处理cookie值中可能包含=的情况 return valueParts.join('='); } }
return null;}
/** * 处理静态资源 * @param {string} path 资源路径 * @returns {Response} 静态资源响应 */function handleStaticResource(path) { if (path === '/login.css') { return new Response(` body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f8f9fa; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; padding: 20px; } .login-container { background-color: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 30px; width: 100%; max-width: 400px; } h1 { margin-top: 0; color: #333; font-size: 24px; text-align: center; } .error-message { color: #dc3545; margin-bottom: 15px; text-align: center; } form { display: flex; flex-direction: column; } input[type="password"] { padding: 10px 12px; border: 1px solid #ced4da; border-radius: 4px; margin-bottom: 15px; font-size: 16px; } button { background-color: #007bff; color: white; border: none; border-radius: 4px; padding: 12px; font-size: 16px; cursor: pointer; transition: background-color 0.2s; } button:hover { background-color: #0069d9; } `, { headers: { 'Content-Type': 'text/css', 'Cache-Control': 'public, max-age=86400' } }); }
if (path === '/login.js') { return new Response(` document.addEventListener('DOMContentLoaded', function() { const form = document.getElementById('login-form'); form.addEventListener('submit', function(e) { const passwordInput = document.getElementById('password'); if (!passwordInput.value.trim()) { e.preventDefault(); showError('请输入密码'); } });
function showError(message) { let errorDiv = document.querySelector('.error-message'); if (!errorDiv) { errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; const h1 = document.querySelector('h1'); h1.parentNode.insertBefore(errorDiv, h1.nextSibling); } errorDiv.textContent = message; } }); `, { headers: { 'Content-Type': 'text/javascript', 'Cache-Control': 'public, max-age=86400' } }); }
throw new ProxyError('Not found', 404);}
/** * 显示登录页面 * @param {string} errorMessage 可选的错误消息 * @returns {Response} 登录页面响应 */function showLoginPage(errorMessage = '') { const html = `<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>请输入访问密码</title> <link rel="stylesheet" href="/login.css"> <script src="/login.js"></script></head><body> <div class="login-container"> <h1>请输入访问密码</h1> ${errorMessage ? `<div class="error-message">${errorMessage}</div>` : ''} <form id="login-form" method="POST" action="/login"> <input type="password" id="password" name="password" placeholder="输入访问密码" required autofocus> <button type="submit">确认</button> </form> </div></body></html> `;
return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html;charset=UTF-8', 'Cache-Control': 'no-store', ...CONFIG.SECURITY_HEADERS } });}
/** * 主请求处理函数 * @param {Request} request 原始请求 * @param {Object} env 环境变量 * @returns {Promise<Response>} 响应 */async function handleRequest(request, env) { // 验证请求方法 - 快速失败原则 if (!CONFIG.ALLOWED_METHODS.has(request.method)) { throw new ProxyError('Method not allowed', 405); }
// 处理POST请求的特殊验证 if (request.method === 'POST') { const contentType = request.headers.get('Content-Type');
// 验证Content-Type - 使用更灵活的检查 if (contentType && !CONFIG.isAllowedContentType(contentType)) { throw new ProxyError('Unsupported Content-Type', 415); }
// 验证请求大小 const contentLength = request.headers.get('content-length'); if (contentLength && parseInt(contentLength, 10) > CONFIG.MAX_BODY_SIZE) { throw new ProxyError('Request entity too large', 413); } }
// 构建目标URL - 避免重复解析 const url = new URL(request.url);
// 排除登录路径和静态资源 if (url.pathname === '/login' || url.pathname === '/login.css' || url.pathname === '/login.js') { throw new ProxyError('Not found', 404); }
const targetUrl = `https://${CONFIG.TARGET_HOST}${url.pathname}${url.search}`;
// 准备并发送请求 const proxyRequest = await createProxyRequest(request, targetUrl);
try { const response = await fetchWithTimeout(proxyRequest, CONFIG.TIMEOUT); // 返回处理后的响应 return createProxyResponse(response, request); } catch (error) { // 更好的错误处理,包括重试逻辑 if (error.name === 'AbortError') { // 对于超时错误,可以考虑重试 console.warn(`Request timed out for ${targetUrl}`); throw new ProxyError('Gateway Timeout', 504); } throw error; }}
/** * 创建代理请求 - 优化头部处理 * @param {Request} originalRequest 原始请求 * @param {string} targetUrl 目标URL */async function createProxyRequest(originalRequest, targetUrl) { // 直接利用Headers API进行头部处理 const headers = new Headers();
// 使用迭代器一次性处理所有头部 for (const [key, value] of originalRequest.headers) { if (!shouldSkipHeader(key)) { headers.set(key, value); } }
// 设置必要的请求头 headers.set('Host', CONFIG.TARGET_HOST); headers.set('Origin', `https://${CONFIG.TARGET_HOST}`); headers.set('Referer', `https://${CONFIG.TARGET_HOST}`);
// 用户代理可能也需要设置 if (!headers.has('User-Agent')) { headers.set('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'); }
// 只在需要时获取CF连接IP const cfIp = originalRequest.headers.get('cf-connecting-ip'); if (cfIp) headers.set('X-Real-IP', cfIp);
// 利用条件判断优化body处理 const needsBody = !['GET', 'HEAD'].includes(originalRequest.method);
// 构建新请求 return new Request(targetUrl, { method: originalRequest.method, headers, body: needsBody ? originalRequest.body : undefined, redirect: 'follow', });}
/** * 创建代理响应 * @param {Response} originalResponse 原始响应 * @param {Request} originalRequest 原始请求 - 用于根据请求类型确定缓存策略 * @returns {Response} 修改后的响应 */function createProxyResponse(originalResponse, originalRequest) { const headers = new Headers();
// 一次性处理所有头部 for (const [key, value] of originalResponse.headers) { if (!shouldSkipHeader(key)) { headers.set(key, value); } }
// 设置安全头,但要注意保留原始内容类型和编码 Object.entries(CONFIG.SECURITY_HEADERS).forEach(([key, value]) => { // 对于非HTML响应,避免设置过于严格的CSP const contentType = originalResponse.headers.get('Content-Type') || ''; if (key === 'Content-Security-Policy' && !contentType.includes('text/html')) { return; // 跳过非HTML内容的CSP } headers.set(key, value); });
// 根据内容类型设置更合适的缓存控制 const contentType = originalResponse.headers.get('Content-Type') || ''; const url = new URL(originalRequest.url);
// 静态资源使用更长的缓存时间 if (isStaticResource(contentType, url.pathname)) { headers.set('Cache-Control', `public, max-age=${CONFIG.STATIC_CACHE_TTL}`); } else { headers.set('Cache-Control', `public, max-age=${CONFIG.CACHE_TTL}`); }
// 创建新响应 return new Response(originalResponse.body, { status: originalResponse.status, statusText: originalResponse.statusText, headers });}
/** * 判断是否为静态资源 * @param {string} contentType 内容类型 * @param {string} pathname 路径 * @returns {boolean} 是否为静态资源 */function isStaticResource(contentType, pathname) { // 通过内容类型判断 if (contentType.includes('image/') || contentType.includes('font/') || contentType.includes('text/css') || contentType.includes('application/javascript')) { return true; }
// 通过文件扩展名判断 const staticExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.css', '.js', '.woff', '.woff2', '.ttf', '.eot']; return staticExtensions.some(ext => pathname.endsWith(ext));}
/** * 优化的头部跳过检查函数 * @param {string} headerName 头部名称 * @returns {boolean} 是否需要跳过 */function shouldSkipHeader(headerName) { const lowerName = headerName.toLowerCase();
// 使用Set的has方法直接检查前缀开头 for (const prefix of CONFIG.SKIP_HEADERS_PREFIX) { if (lowerName.startsWith(prefix)) { return true; } } return false;}
/** * 带超时的fetch * @param {Request} request 请求 * @param {number} timeout 超时时间(ms) * @returns {Promise<Response>} 响应 */async function fetchWithTimeout(request, timeout) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout);
try { const response = await fetch(request, { signal: controller.signal }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); throw error.name === 'AbortError' ? new ProxyError('Request timeout', 504) : error; }}
/** * 错误处理函数 * @param {Error} error 错误对象 * @returns {Response} 错误响应 */function handleError(error) { const status = error instanceof ProxyError ? error.status : 500; const message = error.message || 'Internal Server Error';
// 预定义错误响应头以减少对象创建 const errorHeaders = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...CONFIG.SECURITY_HEADERS };
// 记录错误 console.error(`[ProxyError] ${status}: ${message}`, error.stack);
return new Response( JSON.stringify({ error: message, status }), { status, headers: errorHeaders } );}
用Cloudflare Workers搭建Wikipedia镜像指南
https://fuwari.vercel.app/posts/wikipedia-mirror/