利用Edgeone边缘函数反代TMDB
前言
这是一个利用腾讯Edgeone边缘函数进行代理tmdb接口的项目
前者有利用Vercel代理tmdb接口的仓库,但是呢我的Emby库根本不够用,因为Vercel每月有100GB流量限制,对于我庞大的库来说完全不够,而且每天都在入库新的媒体
该项目可以直接代理TMDB的API以及图片接口,可直接供神医助手以及其他的可以替换TMDB地址的项目

本文将把 https://github.com/qqcomeup/CF-TMDB-Proxy- 的 Cloudflare Worker 代码改写为 Edgeone 边缘函数代码。Edgeone 在国内使用速度上都更快。
部署步骤
1. 准备 Edgeone 账号
先获取 Edgeone 免费名额,具体方式请移步 https://bing.com。
2. 创建边缘函数
- 打开 Edgeone 控制台。
- 找到你的站点,进入站点概览。
- 点击 边缘函数 → 函数管理 → 新建函数 → 创建 Hello World

完成创建后点击“下一步”。
进入函数代码编辑页面。

3. 复制并部署代码
以下是完整的边缘函数代码:
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// EdgeOne 使用标准 addEventListener 监听 fetch 事件
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const pathname = url.pathname;
const search = url.search;
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
"Cross-Origin-Resource-Policy": "cross-origin"
};
function rewriteConfigImages(payload) {
if (!payload || typeof payload !== "object" || !payload.images)
return payload;
const origin = url.origin.replace(/\/$/, "");
const proxyBase = `${origin}/t/p/`;
payload.images.base_url = proxyBase;
payload.images.secure_base_url = proxyBase;
return payload;
}
__name(rewriteConfigImages, "rewriteConfigImages");
if (request.method === "OPTIONS") {
return new Response(null, { status: 200, headers: corsHeaders });
}
const API_KEY = request.headers.get("X-API-Key") || url.searchParams.get("api_key") || url.searchParams.get("key");
const userAgent = request.headers.get("User-Agent") || "";
// EdgeOne 获取客户端真实 IP 的标准请求头是 X-Forwarded-For 或通过 request.eo 获取
const clientIP = request.headers.get("X-Forwarded-For")?.split(',')[0].trim() || "unknown";
// EdgeOne 对应的地理位置信息在 request.eo.geo 中
const country = request.eo?.geo?.countryCode || "unknown";
const suspiciousUA = ["curl", "wget", "python", "scrapy", "spider"];
const isSuspicious = suspiciousUA.some((ua) => userAgent.toLowerCase().includes(ua));
if (userAgent.toLowerCase().includes("bot") && !userAgent.includes("googlebot") || isSuspicious && !userAgent.includes("Mozilla")) {
return new Response(getFake404HTML(), { status: 404, headers: { "Content-Type": "text/html", ...corsHeaders } });
}
const blockedCountries = [];
if (blockedCountries.includes(country)) {
return new Response(getFake404HTML(), { status: 404, headers: { "Content-Type": "text/html", ...corsHeaders } });
}
if (pathname === "/admin/status" && API_KEY && API_KEY.length === 32) {
return new Response(JSON.stringify({
status: "active",
version: "2.0.0",
endpoints: { images: "/t/p/{size}/{path}", api: "/3/{endpoint}" },
client_info: { ip: clientIP, country, ua: userAgent.substring(0, 50) },
security: { api_key_provided: true, request_secure: true },
performance: { cache_enabled: true, compression: true },
timestamp: (new Date()).toISOString()
}), {
headers: { "Content-Type": "application/json", ...corsHeaders }
});
}
if (pathname === "/health" || pathname === "/ping") {
return new Response(JSON.stringify({
status: "ok",
timestamp: (new Date()).toISOString(),
uptime: "active"
}), {
headers: { "Content-Type": "application/json", ...corsHeaders }
});
}
if (pathname === "/" || pathname === "") {
return new Response(getFake404HTML(), {
status: 404,
headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
});
}
// 1. 图片代理部分
if (pathname.startsWith("/t/p/")) {
try {
const imageUrl = `https://image.tmdb.org${pathname}`;
// 注意:EdgeOne 默认遵循源站的缓存策略。
// 如果要强制在 EdgeOne 节点缓存图片,推荐在返回的 Response 中设置 Cache-Control 强缓存,
// 或者在 EdgeOne 控制台的“规则引擎”中为 /t/p/* 配置缓存时间(7天)。
const response = await fetch(imageUrl, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; TMDB-Proxy/1.0)",
"Accept": "image/*"
}
});
if (!response.ok) {
return new Response(getFake404HTML(), {
status: 404,
headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
});
}
// 组装返回给客户端的图片
return new Response(response.body, {
status: response.status,
headers: {
"Content-Type": response.headers.get("Content-Type") || "image/jpeg",
// 在此处加入强缓存响应头,EdgeOne 节点和浏览器均会缓存该图片 7 天
"Cache-Control": "public, max-age=604800, immutable",
"ETag": response.headers.get("ETag") || "",
"Last-Modified": response.headers.get("Last-Modified") || "",
"Content-Length": response.headers.get("Content-Length") || "",
"Vary": "Accept-Encoding",
...corsHeaders
}
});
} catch (error) {
return new Response(getFake404HTML(), {
status: 404,
headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
});
}
}
// 2. API 代理部分
if (pathname.startsWith("/3/")) {
if (!API_KEY) {
return new Response(getFake404HTML(), {
status: 404,
headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
});
}
try {
let apiUrl = `https://api.tmdb.org${pathname}${search}`;
if (!search.includes("api_key=")) {
const separator = search ? "&" : "?";
apiUrl += `${separator}api_key=${API_KEY}`;
}
// 【核心修复】:显式构建标准的、干净的请求头,避免携带原请求中带有你域名的 Host
const forwardHeaders = new Headers();
forwardHeaders.set("Host", "api.tmdb.org");
forwardHeaders.set("Accept", "application/json");
forwardHeaders.set("User-Agent", request.headers.get("User-Agent") || "Mozilla/5.0");
// 注意:暂时移除过多的 Accept-Encoding,让 EdgeOne 自动处理压缩,防止 text() 解析乱码
if (request.headers.has("Accept-Language")) {
forwardHeaders.set("Accept-Language", request.headers.get("Accept-Language"));
}
const response = await fetch(apiUrl, {
method: request.method,
headers: forwardHeaders
});
// 检查源站是否返回错误
if (!response.ok) {
return new Response(JSON.stringify({ error: `TMDB origin error: ${response.status}` }), {
status: response.status,
headers: { "Content-Type": "application/json", ...corsHeaders }
});
}
let responseText = await response.text();
const cacheTime = pathname.includes("configuration") ? 3600 : (
pathname.includes("search") ? 300 : (
pathname.includes("popular") ? 1800 : 600
)
);
if (pathname.startsWith("/3/configuration")) {
try {
const json = JSON.parse(responseText);
responseText = JSON.stringify(rewriteConfigImages(json));
} catch (err) {
// 打印错误日志便于在 EdgeOne 控制台调试
console.error("Failed to parse configuration JSON:", err);
}
}
// 移除可能冲突的底层编码头,让 EdgeOne 统一重新打包
const responseHeaders = new Headers(corsHeaders);
responseHeaders.set("Content-Type", "application/json; charset=utf-8");
responseHeaders.set("Cache-Control", `public, max-age=${cacheTime}`);
return new Response(responseText, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
// 捕捉具体错误返回,方便你排查究竟是 fetch 失败还是 JSON 解析失败
return new Response(JSON.stringify({
error: "Service temporarily unavailable",
details: error.message
}), {
status: 503,
headers: { "Content-Type": "application/json", ...corsHeaders }
});
}
}
return new Response(getFake404HTML(), {
status: 404,
headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
});
}
function getFake404HTML() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 Not Found</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8f9fa; color: #212529; line-height: 1.6; min-height: 100vh;
display: flex; align-items: center; justify-content: center;
}
.error-container { text-align: center; max-width: 600px; padding: 2rem; }
.error-code { font-size: 8rem; font-weight: 300; color: #6c757d; margin-bottom: 1rem; line-height: 1; }
.error-title { font-size: 2rem; font-weight: 400; color: #495057; margin-bottom: 1rem; }
.error-message { font-size: 1.1rem; color: #6c757d; margin-bottom: 2rem; }
.error-details {
background: #e9ecef; border-radius: 8px; padding: 1rem; margin: 1.5rem 0;
font-family: 'Courier New', monospace; font-size: 0.9rem; color: #495057; text-align: left;
}
.back-link {
display: inline-block; padding: 0.75rem 1.5rem; background: #007bff; color: white;
text-decoration: none; border-radius: 4px; transition: background-color 0.2s;
}
.back-link:hover { background: #0056b3; }
.footer { margin-top: 3rem; font-size: 0.9rem; color: #adb5bd; }
.server-info { margin-top: 1rem; font-size: 0.8rem; color: #ced4da; }
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">404</div>
<h1 class="error-title">Page Not Found</h1>
<p class="error-message">The requested resource could not be found on this server.</p>
<div class="error-details">
<strong>Error Details:</strong><br>
\u2022 Request Method: GET<br>
\u2022 Request URL: ${new Date().toISOString().split("T")[0]}<br>
\u2022 Server: Tencent Cloud EdgeOne<br>
\u2022 Timestamp: ${new Date().toISOString()}
</div>
<p style="color: #6c757d; margin: 1.5rem 0;">
If you believe this is an error, please contact the site administrator.
</p>
<a href="javascript:history.back()" class="back-link">\u2190 Go Back</a>
<div class="footer">
<p>This page was generated automatically.</p>
<div class="server-info">Server: EdgeOne Functions | Error Code: HTTP_404_NOT_FOUND</div>
</div>
</div>
<script>
console.log('%c\u{1F3AC} TMDB Proxy Service v2.0 (EdgeOne)', 'color: #007bff; font-size: 16px; font-weight: bold;');
console.log('%cService Status: \u2705 Active (Enhanced)', 'color: #28a745;');
console.log('%cEndpoints:', 'color: #6c757d;');
console.log(' \u2022 Images: /t/p/{size}/{path} (7-day cache)');
console.log(' \u2022 API: /3/{endpoint} (Smart cache 5min-1hr)');
console.log(' \u2022 Health: /health, /ping');
console.log(' \u2022 Admin: /admin/status (requires API key)');
console.log('%cAPI Key Methods:', 'color: #17a2b8;');
console.log(' \u2022 Header: X-API-Key: your_api_key');
console.log(' \u2022 URL Param: ?api_key=your_api_key');
console.log(' \u2022 URL Param: ?key=your_api_key');
console.log('%cFeatures: Cache, Compression, Security, Geo-blocking', 'color: #28a745;');
console.log('%c\u26A0\uFE0F Disguised as 404 for security', 'color: #ffc107;');
window.testAPI = () => fetch('/3/configuration').then(r=>r.json()).then(console.log);
window.testImage = () => { const i=new Image(); i.onload=()=>console.log('\u2705 Image OK'); i.onerror=()=>console.log('\u274C Image failed'); i.src='/t/p/w500/bcP7FtskwsNp1ikpMQJzDPjofP5.jpg'; };
console.log('%cTest: testAPI() | testImage()', 'color: #17a2b8;');
<\/script>
</body>
</html>`;
}
__name(getFake404HTML, "getFake404HTML");这样就成功部署好了。
自定义域名与触发规则
新增自定义托管域名
前往 域名服务 → 域名管理 → 添加域名。

填写自定义域名、源站和缓存规则。

添加触发规则
进入新创建的函数项目,找到 触发规则,点击 新增触发规则,保存即可。

这个步骤相当于 Cloudflare Worker 的域名路由。
验证部署
访问你的域名后缀:
/health/ping
若返回:
{"status":"ok","timestamp":"2026-06-02T12:26:39.808Z","uptime":"active"}表示部署成功,服务正常运行。