这是一个部署在 Cloudflare Workers 上的 Emby 媒体服务器反向代理。

Cloudflare Worker Emby 代理服务使用教程

📌 项目简介

这是一个部署在 Cloudflare Workers 上的 Emby 媒体服务器反向代理,主要功能包括:

  • 🔒 地区限制:仅允许中国大陆 IP 访问(境外 IP 自动拦截)
  • 📊 访问统计:实时记录访问日志,提供可视化统计面板
  • 🔐 密码保护:统计页面需要密码才能查看
  • 🌐 WebSocket 支持:完整支持 Emby 的 WebSocket 连接
  • 📝 详细日志:记录 IP、国家、请求类型、响应时间等信息

🚀 部署步骤

第一步:注册并登录 Cloudflare

  1. 打开浏览器,访问 https://dash.cloudflare.com/sign-up
  2. 输入邮箱地址和密码,完成注册
  3. 登录 Cloudflare 控制台

第二步:将域名接入 Cloudflare(可选)

  1. 在 Cloudflare 控制台首页,点击 添加站点
  2. 输入你的域名(例如 example.com),点击 继续
  3. 选择 Free 套餐,点击 继续
  4. Cloudflare 会扫描你当前的 DNS 记录,确认无误后点击 继续
  5. Cloudflare 会提供两个新的 DNS 服务器地址
  6. 前往你的域名注册商(如阿里云、腾讯云、GoDaddy 等),将域名的 DNS 服务器修改为 Cloudflare 提供的地址
  7. 回到 Cloudflare 控制台,点击 完成,检查名称服务器
  8. 等待 DNS 生效(通常几分钟到几小时,Cloudflare 会发邮件通知)

第三步:创建 KV 命名空间

  1. 在 Cloudflare 控制台左侧菜单中,找到 Workers 和 Pages
  2. 点击展开,选择 KV
  3. 点击右上角的 创建命名空间
  4. 在弹窗中填写命名空间名称:ACCESS_LOGS
  5. 点击 添加 完成创建
  6. 你会看到新创建的命名空间出现在列表中

第四步:创建 Worker

  1. 在左侧菜单中,点击 Workers 和 Pages
  2. 点击 创建应用程序 按钮
  3. 选择 创建 Worker 标签页
  4. 给 Worker 取一个名称,例如 emby-proxy,他会生成属于你的worker地址,例如https://emby-proxy.rooxu.workers.dev
  5. 点击右下角的 部署 按钮
  6. 部署成功后,点击 编辑代码 按钮

第五步:绑定 KV 命名空间

  1. 在 Worker 编辑页面,点击右上角的 设置 标签
  2. 在左侧子菜单中,点击 变量
  3. 向下滚动到 KV 命名空间绑定 部分
  4. 点击 添加绑定 按钮
  5. 在弹窗中:

    • 变量名称:输入 ACCESS_LOGS(必须与代码中完全一致)
    • KV 命名空间:在下拉菜单中选择刚才创建的 ACCESS_LOGS
  6. 点击 保存并部署

第六步:粘贴并修改代码

  1. 回到 Worker 的代码编辑器(点击 资源 标签可回到代码界面)
  2. 删除编辑器中的默认代码
  3. 将完整的项目代码粘贴到编辑器中

    // 配置
    const CONFIG = {
     targetUrl: 'https://your-emby.com:8443',  // 👈 修改为你的 Emby 服务器地址
     statsPassword: 'yourpassword',            // 👈 修改为你的统计页面密码
    };
    // 国家代码映射
    const countryNames = {
     'CN': '中国',
     'US': '美国', 'JP': '日本', 'KR': '韩国', 'GB': '英国',
     'DE': '德国', 'FR': '法国', 'CA': '加拿大', 'AU': '澳大利亚',
     'SG': '新加坡', 'HK': '香港', 'TW': '台湾', 'MO': '澳门',
     'RU': '俄罗斯', 'IN': '印度', 'BR': '巴西', 'IT': '意大利',
     'ES': '西班牙', 'NL': '荷兰', 'SE': '瑞典', 'CH': '瑞士'
    };
    
    // 请求类型映射
    const typeNames = {
     'success': '成功',
     'blocked': '拦截',
     'websocket': 'WebSocket',
     'error': '错误',
     'unknown': '未知'
    };
    
    addEventListener('fetch', event => {
     event.respondWith(handleRequest(event.request))
    });
    
    async function handleRequest(request) {
     // 获取客户端 IP 和地区信息
     const clientIP = request.headers.get('CF-Connecting-IP');
     const country = request.headers.get('CF-IPCountry') || 'XX';
     
     // 1. 拦截非中国大陆 IP(对所有 /* 路径生效,包括 /stats)
     if (country !== 'CN') {
         await logAccessToKV(clientIP, country, false, 'blocked');
         return createBlockedResponse(clientIP, country);
     }
     
     // 2. 检查是否是统计查询请求
     const url = new URL(request.url);
     if (url.pathname === '/stats') {
         return handleStatsRequest(request);
     }
     
     // 3. 处理 OPTIONS 预检请求
     if (request.method === 'OPTIONS') {
         return handleOptions();
     }
     
     // 4. 记录访问开始时间
     const startTime = Date.now();
     
     try {
         // 构造目标 URL
         const url = new URL(request.url);
         const targetUrl = CONFIG.targetUrl + url.pathname + url.search;
         
         // 创建新请求
         const newRequest = new Request(targetUrl, {
             method: request.method,
             headers: request.headers,
             body: request.body,
             redirect: 'follow'
         });
         
         // 处理 WebSocket
         if (request.headers.get('Upgrade') === 'websocket') {
             const wsResponse = await fetch(newRequest);
             await logAccessToKV(clientIP, country, true, 'websocket', startTime);
             return wsResponse;
         }
         
         // 发送请求到 Emby 服务器
         const response = await fetch(newRequest);
         
         // 修改响应头
         const modifiedResponse = new Response(response.body, response);
         modifiedResponse.headers.set('Access-Control-Allow-Origin', '*');
         modifiedResponse.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
         modifiedResponse.headers.set('Access-Control-Allow-Headers', '*');
         modifiedResponse.headers.set('X-Proxy-By', 'Cloudflare-Worker');
         
         // 记录成功访问
         await logAccessToKV(clientIP, country, true, 'success', startTime);
         
         return modifiedResponse;
         
     } catch (error) {
         // 记录错误
         await logAccessToKV(clientIP, country, false, 'error', startTime);
         return new Response('Proxy Error: ' + error.message, {
             status: 502,
             headers: {
                 'Content-Type': 'text/plain;charset=UTF-8',
                 'Access-Control-Allow-Origin': '*'
             }
         });
     }
    }
    
    // 处理 OPTIONS 请求
    function handleOptions() {
     return new Response(null, {
         headers: {
             'Access-Control-Allow-Origin': '*',
             'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
             'Access-Control-Allow-Headers': '*',
             'Access-Control-Max-Age': '86400'
         }
     });
    }
    
    // 格式化时间戳为可读时间
    function formatTimestamp(timestamp) {
     if (!timestamp) return '-';
     const date = new Date(timestamp);
     const year = date.getFullYear();
     const month = String(date.getMonth() + 1).padStart(2, '0');
     const day = String(date.getDate()).padStart(2, '0');
     const hours = String(date.getHours()).padStart(2, '0');
     const minutes = String(date.getMinutes()).padStart(2, '0');
     const seconds = String(date.getSeconds()).padStart(2, '0');
     return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }
    
    // 格式化持续时间为可读格式
    function formatDuration(ms) {
     if (!ms || ms === 0) return '-';
     if (ms < 1000) return `${ms}毫秒`;
     if (ms < 60000) return `${(ms / 1000).toFixed(2)}秒`;
     const minutes = Math.floor(ms / 60000);
     const seconds = ((ms % 60000) / 1000).toFixed(0);
     return `${minutes}分${seconds}秒`;
    }
    
    // 格式化类型分布
    function formatTypes(types) {
     if (!types || Object.keys(types).length === 0) return '-';
     return Object.entries(types)
         .map(([type, count]) => `${typeNames[type] || type}:${count}`)
         .join(' ');
    }
    
    // 使用 KV 存储记录访问日志
    async function logAccessToKV(ip, country, allowed, type, startTime) {
     try {
         const now = Date.now();
         const duration = startTime ? now - startTime : 0;
         
         // 生成当天的日期作为 key 的一部分(按天统计)
         const today = new Date().toISOString().split('T')[0];
         const kvKey = `access:${ip}:${today}`;
         
         // 从 KV 读取现有记录
         let existingData = await ACCESS_LOGS.get(kvKey, 'json');
         
         if (!existingData) {
             existingData = {
                 ip: ip,
                 country: country,
                 firstAccess: now,
                 lastAccess: now,
                 totalRequests: 0,
                 blockedRequests: 0,
                 totalDuration: 0,
                 types: {},
                 dailyAccess: {}
             };
         }
         
         // 更新记录
         existingData.lastAccess = now;
         existingData.totalRequests++;
         existingData.totalDuration += duration || 0;
         
         if (!allowed) {
             existingData.blockedRequests++;
         }
         
         // 记录请求类型统计
         const reqType = type || 'unknown';
         if (!existingData.types[reqType]) {
             existingData.types[reqType] = 0;
         }
         existingData.types[reqType]++;
         
         // 记录每日访问统计
         if (!existingData.dailyAccess[today]) {
             existingData.dailyAccess[today] = {
                 totalRequests: 0,
                 blockedRequests: 0,
                 totalDuration: 0
             };
         }
         existingData.dailyAccess[today].totalRequests++;
         existingData.dailyAccess[today].totalDuration += duration || 0;
         if (!allowed) {
             existingData.dailyAccess[today].blockedRequests++;
         }
         
         // 保存到 KV(设置过期时间为 30 天)
         await ACCESS_LOGS.put(kvKey, JSON.stringify(existingData), {
             expirationTtl: 30 * 24 * 60 * 60 // 30天
         });
         
         // 同时更新全局统计
         await updateGlobalStats(ip, country, allowed, type, duration);
         
     } catch (error) {
         console.error('KV logging error:', error);
     }
    }
    
    // 更新全局统计
    async function updateGlobalStats(ip, country, allowed, type, duration) {
     try {
         const statsKey = 'global:stats';
         let globalStats = await ACCESS_LOGS.get(statsKey, 'json') || {
             totalRequests: 0,
             blockedRequests: 0,
             uniqueIPs: [],
             countries: {},
             lastUpdated: Date.now()
         };
         
         // 更新统计数据
         globalStats.totalRequests++;
         if (!allowed) {
             globalStats.blockedRequests++;
         }
         
         // 记录唯一 IP
         if (!globalStats.uniqueIPs) {
             globalStats.uniqueIPs = [];
         }
         if (!globalStats.uniqueIPs.includes(ip)) {
             globalStats.uniqueIPs.push(ip);
         }
         
         // 记录国家统计
         if (!globalStats.countries[country]) {
             globalStats.countries[country] = 0;
         }
         globalStats.countries[country]++;
         
         globalStats.lastUpdated = Date.now();
         
         // 保存到 KV(设置较长的过期时间)
         await ACCESS_LOGS.put(statsKey, JSON.stringify(globalStats), {
             expirationTtl: 86400 // 24小时
         });
         
     } catch (error) {
         console.error('Global stats update error:', error);
     }
    }
    
    // 处理统计查询请求
    async function handleStatsRequest(request) {
     const url = new URL(request.url);
     const method = request.method;
     
     // 处理退出登录
     if (url.searchParams.get('logout') === '1') {
         return new Response(null, {
             status: 302,
             headers: {
                 'Location': '/stats',
                 'Set-Cookie': 'stats_auth=; Path=/stats; HttpOnly; SameSite=Strict; Max-Age=0',
                 'Access-Control-Allow-Origin': '*'
             }
         });
     }
     
     // GET 请求:显示登录页面或统计页面
     if (method === 'GET') {
         // 检查 cookie 中是否有有效的登录令牌
         const cookieHeader = request.headers.get('Cookie') || '';
         const cookies = parseCookies(cookieHeader);
         
         if (cookies.stats_auth === CONFIG.statsPassword) {
             // 已登录,显示统计页面
             return await showStatsPage();
         }
         
         // 未登录,显示登录页面
         return showLoginPage();
     }
     
     // POST 请求:处理登录
     if (method === 'POST') {
         const contentType = request.headers.get('Content-Type') || '';
         
         let password = '';
         if (contentType.includes('application/json')) {
             const body = await request.json();
             password = body.password || '';
         } else if (contentType.includes('application/x-www-form-urlencoded')) {
             const formData = await request.formData();
             password = formData.get('password') || '';
         }
         
         if (password === CONFIG.statsPassword) {
             // 密码正确,设置 cookie 并重定向到统计页面
             return new Response(null, {
                 status: 302,
                 headers: {
                     'Location': '/stats',
                     'Set-Cookie': `stats_auth=${CONFIG.statsPassword}; Path=/stats; HttpOnly; SameSite=Strict; Max-Age=86400`,
                     'Access-Control-Allow-Origin': '*'
                 }
             });
         } else {
             // 密码错误
             return showLoginPage('密码错误,请重试');
         }
     }
     
     // 其他请求方法
     return new Response('Method Not Allowed', { status: 405 });
    }
    
    // 解析 Cookies
    function parseCookies(cookieHeader) {
     const cookies = {};
     cookieHeader.split(';').forEach(cookie => {
         const parts = cookie.trim().split('=');
         if (parts.length === 2) {
             cookies[parts[0].trim()] = parts[1].trim();
         }
     });
     return cookies;
    }
    
    // 显示登录页面
    function showLoginPage(errorMessage = '') {
     const errorHtml = errorMessage ? `
         <div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 8px; padding: 12px; margin-bottom: 20px; color: #721c24;">
             ${errorMessage}
         </div>
     ` : '';
     
     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>统计页面 - 登录</title>
     <style>
         * { margin: 0; padding: 0; box-sizing: border-box; }
         body {
             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
             min-height: 100vh;
             display: flex;
             align-items: center;
             justify-content: center;
             padding: 20px;
         }
         .container {
             background: white;
             border-radius: 16px;
             padding: 40px;
             max-width: 400px;
             width: 100%;
             box-shadow: 0 20px 60px rgba(0,0,0,0.3);
         }
         h1 {
             color: #1a1a1a;
             font-size: 24px;
             text-align: center;
             margin-bottom: 10px;
         }
         .subtitle {
             color: #666;
             text-align: center;
             margin-bottom: 30px;
             font-size: 14px;
         }
         .form-group {
             margin-bottom: 20px;
         }
         label {
             display: block;
             margin-bottom: 8px;
             color: #333;
             font-weight: 500;
         }
         input {
             width: 100%;
             padding: 12px;
             border: 2px solid #e0e0e0;
             border-radius: 8px;
             font-size: 16px;
             transition: border-color 0.3s;
         }
         input:focus {
             outline: none;
             border-color: #667eea;
         }
         button {
             width: 100%;
             padding: 12px;
             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
             color: white;
             border: none;
             border-radius: 8px;
             font-size: 16px;
             font-weight: 500;
             cursor: pointer;
             transition: transform 0.2s;
         }
         button:hover {
             transform: translateY(-2px);
         }
         button:active {
             transform: translateY(0);
         }
     </style>
    </head>
    <body>
     <div class="container">
         <h1>📊 统计页面</h1>
         <p class="subtitle">请输入密码查看访问统计</p>
         
         ${errorHtml}
         
         <form method="POST" action="/stats">
             <div class="form-group">
                 <label for="password">密码</label>
                 <input type="password" id="password" name="password" placeholder="请输入访问密码" required autofocus>
             </div>
             <button type="submit">登录</button>
         </form>
     </div>
    </body>
    </html>`;
     
     return new Response(html, {
         status: errorMessage ? 401 : 200,
         headers: {
             'Content-Type': 'text/html;charset=UTF-8',
             'Access-Control-Allow-Origin': '*'
         }
     });
    }
    
    // 显示统计页面
    async function showStatsPage() {
     try {
         // 获取全局统计
         const globalStats = await ACCESS_LOGS.get('global:stats', 'json');
         
         // 获取最近的访问记录(列出所有 key)
         const accessKeys = await ACCESS_LOGS.list({ prefix: 'access:' });
         const recentAccess = [];
         
         for (const key of accessKeys.keys) {
             const data = await ACCESS_LOGS.get(key.name, 'json');
             if (data) {
                 // 从 key 中正确提取 IP 和日期
                 // key 格式: access:1.2.3.4:2026-05-14
                 const keyWithoutPrefix = key.name.replace('access:', '');
                 const lastColonIndex = keyWithoutPrefix.lastIndexOf(':');
                 const ip = keyWithoutPrefix.substring(0, lastColonIndex);
                 const date = keyWithoutPrefix.substring(lastColonIndex + 1);
                 
                 recentAccess.push({
                     ip: ip,
                     date: date,
                     ...data
                 });
             }
         }
         
         // 按最后访问时间排序
         recentAccess.sort((a, b) => b.lastAccess - a.lastAccess);
         
         // 生成表格行
         let tableRows = '';
         const displayData = recentAccess.slice(0, 100); // 只显示最近100条
         
         if (displayData.length === 0) {
             tableRows = '<tr><td colspan="9" style="text-align: center; padding: 40px; color: #888;">暂无访问记录</td></tr>';
         } else {
             displayData.forEach((item, index) => {
                 const countryName = countryNames[item.country] || item.country || '未知';
                 const isBlocked = item.blockedRequests > 0;
                 const avgDuration = item.totalRequests > 0 ? Math.round(item.totalDuration / item.totalRequests) : 0;
                 
                 tableRows += `
                 <tr class="${isBlocked ? 'blocked-row' : ''}">
                     <td>${index + 1}</td>
                     <td><code>${item.ip}</code></td>
                     <td>${countryName}</td>
                     <td>${formatTimestamp(item.firstAccess)}</td>
                     <td>${formatTimestamp(item.lastAccess)}</td>
                     <td>${item.totalRequests}</td>
                     <td><span class="${isBlocked ? 'badge-danger' : 'badge-success'}">${item.blockedRequests}</span></td>
                     <td>${formatDuration(avgDuration)}</td>
                     <td><span class="type-badge">${formatTypes(item.types)}</span></td>
                 </tr>`;
             });
         }
         
         // 格式化全局统计
         let globalStatsHtml = '';
         if (globalStats && globalStats.totalRequests > 0) {
             const uniqueIPCount = globalStats.uniqueIPs ? globalStats.uniqueIPs.length : 0;
             const blockRate = globalStats.totalRequests > 0 
                 ? ((globalStats.blockedRequests / globalStats.totalRequests) * 100).toFixed(1) 
                 : 0;
             
             // 格式化国家统计
             let countriesHtml = '';
             if (globalStats.countries) {
                 countriesHtml = Object.entries(globalStats.countries)
                     .sort((a, b) => b[1] - a[1])
                     .map(([code, count]) => `${countryNames[code] || code}: ${count}次`)
                     .join('<br>');
             }
             
             globalStatsHtml = `
             <div class="stats-grid">
                 <div class="stat-card">
                     <div class="stat-label">总请求数</div>
                     <div class="stat-value">${globalStats.totalRequests}</div>
                 </div>
                 <div class="stat-card">
                     <div class="stat-label">拦截请求</div>
                     <div class="stat-value" style="color: #e94560;">${globalStats.blockedRequests}</div>
                 </div>
                 <div class="stat-card">
                     <div class="stat-label">拦截率</div>
                     <div class="stat-value">${blockRate}%</div>
                 </div>
                 <div class="stat-card">
                     <div class="stat-label">独立IP数</div>
                     <div class="stat-value">${uniqueIPCount}</div>
                 </div>
                 <div class="stat-card">
                     <div class="stat-label">最后更新</div>
                     <div class="stat-value" style="font-size: 14px;">${formatTimestamp(globalStats.lastUpdated)}</div>
                 </div>
             </div>
             <div class="section">
                 <h3>🌍 国家/地区分布</h3>
                 <div class="countries-list">${countriesHtml || '暂无数据'}</div>
             </div>`;
         } else {
             globalStatsHtml = '<div class="no-data">📭 暂无统计数据,请等待有访问请求后查看</div>';
         }
         
         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>访问统计</title>
     <style>
         * { margin: 0; padding: 0; box-sizing: border-box; }
         body {
             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
             background: #1a1a2e;
             color: #e0e0e0;
             padding: 20px;
             line-height: 1.6;
         }
         .container {
             max-width: 1400px;
             margin: 0 auto;
         }
         .header {
             display: flex;
             justify-content: space-between;
             align-items: center;
             margin-bottom: 30px;
             flex-wrap: wrap;
             gap: 15px;
         }
         h1 {
             color: #667eea;
             font-size: 28px;
         }
         .logout-btn {
             display: inline-block;
             padding: 10px 20px;
             background: #e94560;
             color: white;
             text-decoration: none;
             border-radius: 6px;
             font-size: 14px;
             transition: background 0.3s;
         }
         .logout-btn:hover {
             background: #c73e54;
         }
         .stats-grid {
             display: grid;
             grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
             gap: 15px;
             margin-bottom: 30px;
         }
         .stat-card {
             background: #16213e;
             border: 1px solid #0f3460;
             border-radius: 10px;
             padding: 20px;
             text-align: center;
         }
         .stat-label {
             font-size: 13px;
             color: #888;
             margin-bottom: 8px;
             text-transform: uppercase;
         }
         .stat-value {
             font-size: 32px;
             font-weight: bold;
             color: #00ff88;
         }
         .section {
             background: #16213e;
             border: 1px solid #0f3460;
             border-radius: 10px;
             padding: 20px;
             margin-bottom: 20px;
         }
         .section h2 {
             color: #e94560;
             margin-bottom: 20px;
             font-size: 20px;
             display: flex;
             align-items: center;
             gap: 10px;
         }
         .section h3 {
             color: #667eea;
             margin-bottom: 15px;
             font-size: 18px;
         }
         .no-data {
             text-align: center;
             padding: 40px;
             color: #888;
             font-size: 16px;
         }
         .table-wrapper {
             overflow-x: auto;
         }
         table {
             width: 100%;
             border-collapse: collapse;
             font-size: 14px;
             min-width: 900px;
         }
         th {
             background: #0f3460;
             color: #667eea;
             padding: 12px 10px;
             text-align: left;
             font-weight: 600;
             font-size: 13px;
             white-space: nowrap;
         }
         td {
             padding: 10px;
             border-bottom: 1px solid #0f3460;
             white-space: nowrap;
         }
         tr:hover {
             background: rgba(102, 126, 234, 0.05);
         }
         .blocked-row {
             background: rgba(233, 69, 96, 0.05);
         }
         .blocked-row:hover {
             background: rgba(233, 69, 96, 0.1);
         }
         code {
             background: #0f3460;
             padding: 2px 8px;
             border-radius: 4px;
             font-size: 13px;
             color: #00ff88;
         }
         .badge-success {
             display: inline-block;
             padding: 2px 8px;
             border-radius: 4px;
             background: rgba(0, 255, 136, 0.15);
             color: #00ff88;
             font-size: 12px;
         }
         .badge-danger {
             display: inline-block;
             padding: 2px 8px;
             border-radius: 4px;
             background: rgba(233, 69, 96, 0.15);
             color: #e94560;
             font-size: 12px;
         }
         .type-badge {
             font-size: 12px;
             color: #aaa;
         }
         .countries-list {
             color: #ccc;
             line-height: 2;
         }
         .refresh-info {
             text-align: center;
             color: #666;
             font-size: 12px;
             margin-top: 20px;
             padding: 15px;
         }
     </style>
    </head>
    <body>
     <div class="container">
         <div class="header">
             <h1>📊 访问统计面板</h1>
             <div>
                 <a href="/stats?logout=1" class="logout-btn">退出登录</a>
             </div>
         </div>
         
         <div class="section">
             <h2>📈 全局统计</h2>
             ${globalStatsHtml}
         </div>
         
         <div class="section">
             <h2>📋 最近访问记录 <span style="font-size: 14px; color: #888;">(最近${Math.min(displayData.length, 100)}条)</span></h2>
             <div class="table-wrapper">
                 <table>
                     <thead>
                         <tr>
                             <th>#</th>
                             <th>IP 地址</th>
                             <th>国家/地区</th>
                             <th>首次访问</th>
                             <th>最后访问</th>
                             <th>总请求</th>
                             <th>拦截数</th>
                             <th>平均响应</th>
                             <th>请求类型</th>
                         </tr>
                     </thead>
                     <tbody>
                         ${tableRows}
                     </tbody>
                 </table>
             </div>
         </div>
         
         <div class="refresh-info">
             📅 页面生成时间:${formatTimestamp(Date.now())} | 数据实时从 KV 存储读取
         </div>
     </div>
    </body>
    </html>`;
         
         return new Response(html, {
             headers: {
                 'Content-Type': 'text/html;charset=UTF-8',
                 'Access-Control-Allow-Origin': '*'
             }
         });
         
     } catch (error) {
         return new Response(JSON.stringify({ error: error.message }, null, 2), {
             status: 500,
             headers: {
                 'Content-Type': 'application/json;charset=UTF-8',
                 'Access-Control-Allow-Origin': '*'
             }
         });
     }
    }
    
    // 创建拦截页面
    function createBlockedResponse(clientIP, country) {
     const countryName = countryNames[country] || country;
     const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
     
     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>访问被拒绝</title>
     <style>
         * { margin: 0; padding: 0; box-sizing: border-box; }
         body {
             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
             min-height: 100vh;
             display: flex;
             align-items: center;
             justify-content: center;
             padding: 20px;
         }
         .container {
             background: white;
             border-radius: 16px;
             padding: 40px;
             max-width: 500px;
             width: 100%;
             box-shadow: 0 20px 60px rgba(0,0,0,0.3);
         }
         .icon { text-align: center; margin-bottom: 20px; }
         .icon svg { width: 64px; height: 64px; }
         h1 { 
             color: #1a1a1a;
             font-size: 24px;
             text-align: center;
             margin-bottom: 10px;
         }
         .subtitle {
             color: #666;
             text-align: center;
             margin-bottom: 30px;
             font-size: 14px;
         }
         .info-box {
             background: #f8f9fa;
             border-radius: 8px;
             padding: 20px;
             margin-bottom: 20px;
         }
         .info-item {
             display: flex;
             justify-content: space-between;
             padding: 8px 0;
             border-bottom: 1px solid #e9ecef;
         }
         .info-item:last-child { border-bottom: none; }
         .label { color: #666; font-size: 14px; }
         .value { color: #333; font-weight: 500; font-size: 14px; }
         .tips {
             background: #fff3cd;
             border: 1px solid #ffc107;
             border-radius: 8px;
             padding: 15px;
             margin-top: 20px;
         }
         .tips h3 { color: #856404; margin-bottom: 10px; font-size: 16px; }
         .tips p { color: #856404; font-size: 14px; line-height: 1.6; margin: 5px 0; }
         .footer {
             text-align: center;
             margin-top: 20px;
             color: #999;
             font-size: 12px;
         }
     </style>
    </head>
    <body>
     <div class="container">
         <div style="text-align: center; margin-bottom: 30px;">
             <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2">
                 <circle cx="12" cy="12" r="10"></circle>
                 <line x1="12" y1="8" x2="12" y2="12"></line>
                 <line x1="12" y1="16" x2="12.01" y2="16"></line>
             </svg>
         </div>
         <h1 style="text-align: center; color: #1a1a1a; margin-bottom: 10px;">访问被拒绝</h1>
         <p class="subtitle">此服务仅限中国大陆地区访问</p>
         
         <div class="info-box">
             <div class="info-item">
                 <span class="label">IP 地址</span>
                 <span class="value">${clientIP}</span>
             </div>
             <div class="info-item">
                 <span class="label">所在地区</span>
                 <span class="value">${countryName}</span>
             </div>
             <div class="info-item">
                 <span class="label">访问时间</span>
                 <span class="value">${now}</span>
             </div>
             <div class="info-item">
                 <span class="label">访问状态</span>
                 <span style="color: #dc3545; font-weight: bold;">已拦截</span>
             </div>
         </div>
         
         <div class="tips">
             <h3>💡 可能的原因</h3>
             <p>• 您正在使用非中国大陆的 IP 地址</p>
             <p>• 您可能开启了 VPN 或代理服务</p>
             <p>• 您的网络被识别为境外 IP</p>
         </div>
         
         <div class="tips" style="background: #d4edda; border-color: #28a745;">
             <h3 style="color: #155724;">🔧 解决方法</h3>
             <p style="color: #155724;">• 关闭 VPN 或代理后重试</p>
             <p style="color: #155724;">• 使用中国大陆的网络访问</p>
             <p style="color: #155724;">• 如确认在国内,请联系管理员</p>
         </div>
         
         <div class="footer">
             <p>Request ID: ${crypto.randomUUID()}</p>
             <p>© ${new Date().getFullYear()} Emby Proxy Service</p>
         </div>
     </body>
     </html>`;
     
     return new Response(html, {
         status: 403,
         headers: {
             'Content-Type': 'text/html;charset=UTF-8',
             'Access-Control-Allow-Origin': '*',
             'Cache-Control': 'no-cache'
         }
     });
    }
  4. 找到代码开头的配置部分,根据你的实际情况修改:
// 配置
const CONFIG = {
    targetUrl: 'https://your-emby.com:8443',  // 👈 修改为你的 Emby 服务器地址
    statsPassword: 'yourpassword',            // 👈 修改为你的统计页面密码
};

5.修改完成后,点击右上角的 部署 按钮
6.等待部署完成,页面会提示"部署成功"

第七步:配置自定义域名(可选)

1.在 Worker 详情页面,点击 触发器 标签
2.找到 自定义域 部分
3.点击 添加自定义域
4.在弹出的对话框中输入你想使用的子域名,例如 emby.yourdomain.com
5.点击 添加自定义域
6.等待几分钟后,即可通过 https://emby.yourdomain.com 访问你的 Emby 服务


📖使用说明

正常访问 Emby

部署完成后,直接访问worker地址如:https://emby-proxy.rooxu.workers.dev即可使用 Emby,或者访问你配置的域名。

✅ 中国大陆 IP:正常访问 Emby 服务

❌ 境外 IP:看到拦截提示页面

查看访问统计

访问https://emby-proxy.rooxu.workers.dev/stats
输入统计页面密码(默认:yourpassword

登录后可以查看:
1.全局统计:总请求数、拦截数、拦截率、独立 IP 数
2.国家/地区分布:各国家访问次数统计
3.最近访问记录:最近 100 条详细访问日志