main2 / server.js
qsyhh's picture
Update server.js
4012275 verified
Raw
History Blame Contribute Delete
27.9 kB
/**
* HF Mirror Proxy - 重写版 v2
* 改进点:
* 1. LRU 缓存替代无界 Map,防止内存泄漏
* 2. 精确域名匹配,避免误替换(修复 xethub/cas-bridge CDN 问题)
* 3. 缓存 key 包含 host,支持多域名部署
* 4. 结构化日志,便于排查问题
* 5. 请求限流,防止滥用
* 6. 更好的错误处理和重试机制
* 7. 流式响应优化,大文件下载更稳定
*/
import * as http from "node:http";
import * as https from "node:https";
import * as crypto from "node:crypto";
import * as zlib from "node:zlib";
import { URL } from "node:url";
// ========== 配置 ==========
const CONFIG = {
UPSTREAM: process.env.UPSTREAM || "https://huggingface.co",
PORT: parseInt(process.env.PORT) || 7860,
CACHE_TTL: parseInt(process.env.CACHE_TTL) || 0,
CACHE_MAX_SIZE: parseInt(process.env.CACHE_MAX_SIZE) || 5000,
CACHE_MAX_BYTES: parseInt(process.env.CACHE_MAX_BYTES) || 100 * 1024 * 1024, // 100MB
MAX_RETRIES: parseInt(process.env.MAX_RETRIES) || 3,
PUBLIC_HOST: process.env.PUBLIC_HOST || "",
UPLOAD_TOKEN_TTL: parseInt(process.env.UPLOAD_TOKEN_TTL) || 3600_000, // 1小时
UPLOAD_TOKEN_MAX: parseInt(process.env.UPLOAD_TOKEN_MAX) || 2000,
RATE_LIMIT_WINDOW: parseInt(process.env.RATE_LIMIT_WINDOW) || 60_000, // 1分钟
RATE_LIMIT_MAX: parseInt(process.env.RATE_LIMIT_MAX) || 9999999, // 每分钟最大请求数
REQUEST_TIMEOUT: parseInt(process.env.REQUEST_TIMEOUT) || 300_000, // 5分钟
LOG_LEVEL: process.env.LOG_LEVEL || "info", // debug, info, warn, error
};
// ========== 精确域名判断 ==========
// HF 核心域名 - 可以安全替换为代理域名
const HF_HOSTS = new Set([
"huggingface.co",
"www.huggingface.co",
"hf.co",
"www.hf.co",
]);
const HF_SUFFIXES = [".huggingface.co", ".hf.co"];
// 外部 CDN / S3 桥接域名 - 虽然是 .hf.co 但不能直接替换域名
// 这些域名提供的是带签名的临时 URL,必须通过 token 代理转发
const EXTERNAL_CDN_HOSTS = new Set([
"cas-bridge.xethub.hf.co",
"xethub.hf.co",
"cdn.hf.co",
]);
// 判断是否为外部 CDN(需要 token 代理)
function isExternalCDN(hostname) {
if (EXTERNAL_CDN_HOSTS.has(hostname)) return true;
// 通配匹配 xethub 相关域名
if (hostname.includes("xethub") || hostname.includes("cas-bridge")) return true;
return false;
}
// 判断是否为 HuggingFace 内部域名(可以直接替换为代理域名)
function isHuggingFaceHost(hostname) {
// 先排除外部 CDN
if (isExternalCDN(hostname)) return false;
if (HF_HOSTS.has(hostname)) return true;
return HF_SUFFIXES.some((suffix) => hostname.endsWith(suffix));
}
// ========== 日志系统 ==========
const LOG_COLORS = {
debug: "\x1b[36m", // cyan
info: "\x1b[32m", // green
warn: "\x1b[33m", // yellow
error: "\x1b[31m", // red
reset: "\x1b[0m",
};
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
function log(level, message, meta = {}) {
if (LOG_LEVELS[level] < LOG_LEVELS[CONFIG.LOG_LEVEL]) return;
const timestamp = new Date().toISOString();
const color = LOG_COLORS[level] || "";
const reset = LOG_COLORS.reset;
const metaStr = Object.keys(meta).length ? " " + JSON.stringify(meta) : "";
console.log(`${color}[${timestamp}] [${level.toUpperCase()}]${reset} ${message}${metaStr}`);
}
// ========== LRU 缓存实现 ==========
class LRUCache {
constructor(options) {
this.max = options.max || 1000;
this.maxBytes = options.maxBytes || Infinity;
this.ttl = options.ttl || 0;
this.map = new Map();
this.currentBytes = 0;
}
_makeKey(method, url, host) {
return `${host}:${method}:${url}`;
}
get(method, url, host) {
const key = this._makeKey(method, url, host);
const entry = this.map.get(key);
if (!entry) return null;
// TTL 过期检查
if (this.ttl > 0 && Date.now() - entry.time > entry.ttl) {
this._deleteEntry(key, entry);
return null;
}
// 移动到末尾(最近使用)
this.map.delete(key);
this.map.set(key, entry);
return entry;
}
set(method, url, host, status, headers, body, customTtl) {
const ttl = customTtl || this.ttl;
if (ttl <= 0) return false;
const key = this._makeKey(method, url, host);
const entry = {
time: Date.now(),
ttl,
status,
headers: { ...headers },
body,
size: body?.length || 0,
};
// 如果已存在,先删除旧条目
if (this.map.has(key)) {
this._deleteEntry(key, this.map.get(key));
}
// 空间不足时淘汰最旧的
while (
this.map.size >= this.max ||
(this.maxBytes !== Infinity && this.currentBytes + entry.size > this.maxBytes)
) {
const firstKey = this.map.keys().next().value;
if (!firstKey) break;
this._deleteEntry(firstKey, this.map.get(firstKey));
}
this.map.set(key, entry);
this.currentBytes += entry.size;
return true;
}
_deleteEntry(key, entry) {
this.currentBytes -= entry?.size || 0;
this.map.delete(key);
}
clear() {
this.map.clear();
this.currentBytes = 0;
}
get stats() {
return {
size: this.map.size,
bytes: this.currentBytes,
max: this.max,
maxBytes: this.maxBytes,
};
}
}
// ========== 限流器 ==========
class RateLimiter {
constructor(windowMs, maxRequests) {
this.windowMs = windowMs;
this.maxRequests = maxRequests;
this.clients = new Map();
}
isAllowed(clientId) {
const now = Date.now();
const record = this.clients.get(clientId);
if (!record) {
this.clients.set(clientId, { count: 1, resetTime: now + this.windowMs });
return { allowed: true, remaining: this.maxRequests - 1 };
}
if (now > record.resetTime) {
record.count = 1;
record.resetTime = now + this.windowMs;
return { allowed: true, remaining: this.maxRequests - 1 };
}
if (record.count >= this.maxRequests) {
return {
allowed: false,
remaining: 0,
retryAfter: Math.ceil((record.resetTime - now) / 1000),
};
}
record.count++;
return { allowed: true, remaining: this.maxRequests - record.count };
}
// 定期清理过期记录
cleanup() {
const now = Date.now();
for (const [id, record] of this.clients) {
if (now > record.resetTime + this.windowMs) {
this.clients.delete(id);
}
}
}
get stats() {
return { clients: this.clients.size };
}
}
// ========== 上传 Token 管理(LRU) ==========
class UploadTokenManager {
constructor(maxSize, ttl) {
this.max = maxSize;
this.ttl = ttl;
this.map = new Map();
}
create(originalUrl, host) {
// 空间不足时淘汰最旧的
while (this.map.size >= this.max) {
const firstKey = this.map.keys().next().value;
this.map.delete(firstKey);
}
const token = crypto.randomBytes(16).toString("hex"); // 32字符,更安全
this.map.set(token, { url: originalUrl, time: Date.now() });
return `https://${host}/proxy-upload/${token}`;
}
get(token) {
const entry = this.map.get(token);
if (!entry) return null;
if (Date.now() - entry.time > this.ttl) {
this.map.delete(token);
return null;
}
// 更新使用时间(LRU)
this.map.delete(token);
this.map.set(token, entry);
return entry;
}
clear() {
this.map.clear();
}
get stats() {
return { size: this.map.size, max: this.max };
}
}
// ========== 初始化 ==========
const cache = new LRUCache({
max: CONFIG.CACHE_MAX_SIZE,
maxBytes: CONFIG.CACHE_MAX_BYTES,
ttl: CONFIG.CACHE_TTL,
});
const urlMap = new UploadTokenManager(CONFIG.UPLOAD_TOKEN_MAX, CONFIG.UPLOAD_TOKEN_TTL);
const rateLimiter = new RateLimiter(CONFIG.RATE_LIMIT_WINDOW, CONFIG.RATE_LIMIT_MAX);
// 定期清理限流器
setInterval(() => rateLimiter.cleanup(), 60_000);
// ========== 主代理 ==========
function proxyRequest(clientReq, clientRes) {
const method = clientReq.method;
const url = clientReq.url;
const clientIp = getClientIP(clientReq);
// 限流检查
const rateCheck = rateLimiter.isAllowed(clientIp);
if (!rateCheck.allowed) {
log("warn", "Rate limit exceeded", { ip: clientIp, url });
clientRes.writeHead(429, {
"content-type": "application/json",
"access-control-allow-origin": "*",
"x-ratelimit-limit": String(CONFIG.RATE_LIMIT_MAX),
"x-ratelimit-remaining": "0",
"retry-after": String(rateCheck.retryAfter),
});
clientRes.end(
JSON.stringify({
error: "rate_limited",
message: `Too many requests. Retry after ${rateCheck.retryAfter}s`,
})
);
return;
}
// 上传代理路由
const uploadMatch = url.match(/^\/proxy-upload\/([a-f0-9]{32})(\/.*)?$/);
if (uploadMatch) {
handleUploadProxy(clientReq, clientRes, uploadMatch[1], uploadMatch[2] || "");
return;
}
const bodyChunks = [];
clientReq.on("data", (chunk) => bodyChunks.push(chunk));
clientReq.on("end", () => {
const body = Buffer.concat(bodyChunks);
const host = getHost(clientReq);
// 缓存命中(仅 GET 非文件下载)
if (method === "GET" && !isFileDownloadUrl(url)) {
const cached = cache.get(method, url, host);
if (cached) {
log("debug", "Cache HIT", { url, host });
clientRes.writeHead(cached.status, {
...cached.headers,
"x-cache": "HIT",
"access-control-allow-origin": "*",
"x-ratelimit-remaining": String(rateCheck.remaining),
});
clientRes.end(cached.body);
return;
}
}
doProxy(method, url, clientReq.headers, body, 0, clientRes, host);
});
}
function doProxy(method, url, reqHeaders, body, attempt, clientRes, host) {
const upstreamUrl = new URL(url, CONFIG.UPSTREAM);
const options = {
method,
headers: filterHeaders(reqHeaders),
timeout: CONFIG.REQUEST_TIMEOUT,
};
const startTime = Date.now();
const proxyReq = https.request(upstreamUrl, options, (proxyRes) => {
const statusCode = proxyRes.statusCode;
const respHeaders = filterProxyHeaders(proxyRes.headers);
const contentType = respHeaders["content-type"] || "";
log("debug", "Upstream response", {
method,
url,
status: statusCode,
attempt: attempt + 1,
});
// --- 处理重定向 ---
if ([301, 302, 307, 308].includes(statusCode) && respHeaders["location"]) {
handleRedirect(respHeaders["location"], upstreamUrl, host, clientRes, proxyRes, statusCode);
return;
}
// --- 文件下载 / 流式传输 ---
if (isFileDownloadUrl(url) || statusCode === 206) {
clientRes.writeHead(statusCode, {
...respHeaders,
"access-control-allow-origin": "*",
"x-cache": "MISS",
});
proxyRes.pipe(clientRes);
return;
}
// --- 响应体需改写 ---
const needsBodyRewrite = shouldRewriteBody(contentType, url);
if (!needsBodyRewrite) {
clientRes.writeHead(statusCode, {
...respHeaders,
"access-control-allow-origin": "*",
"x-cache": "MISS",
});
proxyRes.pipe(clientRes);
return;
}
// --- 缓冲后改写响应体 ---
const chunks = [];
proxyRes.on("data", (chunk) => chunks.push(chunk));
proxyRes.on("end", () => {
let fullBody = Buffer.concat(chunks);
const enc = proxyRes.headers["content-encoding"];
try {
if (enc === "gzip") fullBody = zlib.gunzipSync(fullBody);
else if (enc === "deflate") fullBody = zlib.inflateSync(fullBody);
else if (enc === "br") {
// brotli 解压
fullBody = zlib.brotliDecompressSync(fullBody);
}
} catch (decompressErr) {
log("warn", "Decompression failed", { url, error: decompressErr.message });
// 继续使用原始 body
}
let newBody;
const outHeaders = { ...respHeaders };
delete outHeaders["content-encoding"];
delete outHeaders["content-length"];
try {
if (contentType.includes("application/json") || contentType.includes("application/vnd.git-lfs+json")) {
const json = JSON.parse(fullBody.toString("utf-8"));
const rewritten = rewriteBody(json, host);
newBody = Buffer.from(JSON.stringify(rewritten), "utf-8");
} else if (contentType.includes("text/html")) {
let html = fullBody.toString("utf-8");
html = rewriteHtml(html, host);
newBody = Buffer.from(html, "utf-8");
} else {
newBody = fullBody;
}
outHeaders["content-length"] = newBody.length;
outHeaders["access-control-allow-origin"] = "*";
outHeaders["x-cache"] = "MISS";
// 设置缓存(仅 GET 200 成功响应)
if (method === "GET" && statusCode === 200) {
const customTtl = url.includes("/tree?") ? 60_000 : CONFIG.CACHE_TTL;
cache.set(method, url, host, statusCode, outHeaders, newBody, customTtl);
}
clientRes.writeHead(statusCode, outHeaders);
clientRes.end(newBody);
} catch (err) {
log("error", "Body rewrite failed", { url, error: err.message });
clientRes.writeHead(statusCode, {
...respHeaders,
"access-control-allow-origin": "*",
"x-cache": "MISS",
});
clientRes.end(fullBody);
}
});
});
// --- 错误处理 ---
proxyReq.on("error", (err) => {
const elapsed = Date.now() - startTime;
if (attempt < CONFIG.MAX_RETRIES - 1) {
const wait = Math.min(1000 * 2 ** attempt, 10_000);
log("warn", "Retrying request", {
method,
url,
attempt: attempt + 1,
maxRetries: CONFIG.MAX_RETRIES,
error: err.message,
elapsedMs: elapsed,
});
setTimeout(() => doProxy(method, url, reqHeaders, body, attempt + 1, clientRes, host), wait);
} else {
log("error", "Upstream unreachable", {
method,
url,
error: err.message,
elapsedMs: elapsed,
});
if (!clientRes.headersSent) {
clientRes.writeHead(502, {
"content-type": "application/json",
"access-control-allow-origin": "*",
});
clientRes.end(
JSON.stringify({
error: "upstream_unreachable",
message: err.message,
})
);
}
}
});
proxyReq.on("timeout", () => {
proxyReq.destroy();
const elapsed = Date.now() - startTime;
if (attempt < CONFIG.MAX_RETRIES - 1) {
log("warn", "Request timeout, retrying", { method, url, attempt: attempt + 1, elapsedMs: elapsed });
setTimeout(() => doProxy(method, url, reqHeaders, body, attempt + 1, clientRes, host), 1000);
} else {
log("error", "Upstream timeout", { method, url, elapsedMs: elapsed });
if (!clientRes.headersSent) {
clientRes.writeHead(504, {
"content-type": "application/json",
"access-control-allow-origin": "*",
});
clientRes.end(JSON.stringify({ error: "upstream_timeout" }));
}
}
});
if (body.length > 0) proxyReq.write(body);
proxyReq.end();
}
// ========== 重定向处理 ==========
function handleRedirect(location, upstreamUrl, host, clientRes, proxyRes, statusCode) {
try {
const loc = new URL(location, upstreamUrl);
const locHostname = loc.hostname;
log("debug", "Processing redirect", {
original: location,
hostname: locHostname,
isHF: isHuggingFaceHost(locHostname),
isExternalCDN: isExternalCDN(locHostname),
});
if (isHuggingFaceHost(locHostname)) {
// HF 内部重定向 → 替换域名
loc.hostname = host;
loc.protocol = "https:";
log("info", "HF redirect rewritten", {
status: statusCode,
original: location,
rewritten: loc.href,
});
} else {
// 外部 URL (S3/Xet Bridge/CDN等) → token 代理
loc.href = urlMap.create(loc.href, host);
log("info", "External redirect tokenized", {
status: statusCode,
original: location,
rewritten: loc.href,
cdn: locHostname,
});
}
const respHeaders = filterProxyHeaders(proxyRes.headers);
respHeaders["location"] = loc.href;
clientRes.writeHead(statusCode, {
...respHeaders,
"access-control-allow-origin": "*",
});
proxyRes.pipe(clientRes);
} catch (err) {
log("error", "Redirect handling failed", { location, error: err.message });
clientRes.writeHead(502, {
"content-type": "application/json",
"access-control-allow-origin": "*",
});
clientRes.end(JSON.stringify({ error: "redirect_handling_failed", message: err.message }));
}
}
// ========== 响应体改写 ==========
function rewriteBody(obj, host) {
if (!obj || typeof obj !== "object") return obj;
if (Array.isArray(obj)) {
return obj.map((item) => rewriteBody(item, host));
}
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "string") {
let v = value;
v = rewriteUrl(v, host);
if (isExternalUploadField(key, v, host)) {
v = urlMap.create(v, host);
}
result[key] = v;
} else if (typeof value === "object" && value !== null) {
result[key] = rewriteBody(value, host);
} else {
result[key] = value;
}
}
return result;
}
function rewriteUrl(value, host) {
if (typeof value !== "string") return value;
// 精确替换 huggingface.co 域名
value = value.replace(/https?:\/\/huggingface\.co/g, `https://${host}`);
// 精确替换已知的 HF CDN 域名(这些是可以直接替换的内部域名)
value = value.replace(/https?:\/\/cdn-lfs\.hf\.co/g, `https://${host}`);
value = value.replace(/https?:\/\/cdn-lfs-us-1\.hf\.co/g, `https://${host}`);
value = value.replace(/https?:\/\/cdn-lfs-eu-1\.hf\.co/g, `https://${host}`);
value = value.replace(/https?:\/\/cdn-lfs-ap-1\.hf\.co/g, `https://${host}`);
// 注意:xethub / cas-bridge 域名不在这里替换
// 它们会在 isExternalUploadField 中通过 token 代理处理
return value;
}
function rewriteHtml(html, host) {
if (typeof html !== "string") return html;
return rewriteUrl(html, host);
}
function isExternalUploadField(key, value, host) {
if (!value.startsWith("https://")) return false;
if (value.startsWith(`https://${host}`)) return false;
// 检查是否为外部 CDN URL
try {
const url = new URL(value);
if (isExternalCDN(url.hostname)) return true;
} catch {
// 无效 URL,忽略
}
const extFields = [
"href",
"uploadUrl",
"casUrl",
"refreshWriteTokenUrl",
"reconstructionUrl",
"refreshUrl",
"downloadUrl",
"lfs",
];
return extFields.includes(key);
}
function shouldRewriteBody(contentType, url) {
if (contentType.includes("application/json")) return true;
if (contentType.includes("application/vnd.git-lfs+json")) return true;
if (contentType.includes("text/html")) return true;
if (url.includes("/preupload/")) return true;
if (url.includes("/xet-write-token")) return true;
return false;
}
function isFileDownloadUrl(url) {
return url.includes("/resolve/") || url.includes("/raw/");
}
// ========== 上传代理 ==========
function handleUploadProxy(clientReq, clientRes, token, extraPath) {
const entry = urlMap.get(token);
if (!entry) {
log("warn", "Unknown upload token", { token: token.slice(0, 8) + "..." });
clientRes.writeHead(404, {
"content-type": "application/json",
"access-control-allow-origin": "*",
});
clientRes.end(JSON.stringify({ error: "unknown_upload_token" }));
return;
}
const targetUrl = new URL(entry.url + extraPath);
const bodyChunks = [];
clientReq.on("data", (chunk) => bodyChunks.push(chunk));
clientReq.on("end", () => {
const body = Buffer.concat(bodyChunks);
const options = {
method: clientReq.method,
headers: filterHeaders(clientReq.headers),
timeout: CONFIG.REQUEST_TIMEOUT,
};
const req2 = https.request(targetUrl, options, (res2) => {
const respHeaders = filterProxyHeaders(res2.headers);
if ([301, 302, 307, 308].includes(res2.statusCode) && respHeaders["location"]) {
try {
const loc = new URL(respHeaders["location"], targetUrl);
if (isHuggingFaceHost(loc.hostname)) {
const host = getHost(clientReq);
loc.hostname = host;
loc.protocol = "https:";
respHeaders["location"] = loc.href;
}
} catch (err) {
log("warn", "Upload redirect rewrite failed", { error: err.message });
}
}
clientRes.writeHead(res2.statusCode, {
...respHeaders,
"access-control-allow-origin": "*",
});
res2.pipe(clientRes);
});
req2.on("error", (err) => {
log("error", "Upload forward failed", {
target: targetUrl.href,
error: err.message,
});
if (!clientRes.headersSent) {
clientRes.writeHead(502, {
"content-type": "application/json",
"access-control-allow-origin": "*",
});
clientRes.end(
JSON.stringify({
error: "upload_forward_failed",
message: err.message,
})
);
}
});
req2.on("timeout", () => {
req2.destroy();
log("error", "Upload timeout", { target: targetUrl.href });
if (!clientRes.headersSent) {
clientRes.writeHead(504, { "access-control-allow-origin": "*" });
clientRes.end();
}
});
if (body.length > 0) req2.write(body);
req2.end();
});
}
// ========== 工具函数 ==========
function getHost(req) {
return (
CONFIG.PUBLIC_HOST ||
req.headers["x-forwarded-host"] ||
req.headers["host"] ||
`localhost:${CONFIG.PORT}`
);
}
function getClientIP(req) {
return (
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
req.headers["x-real-ip"] ||
req.socket?.remoteAddress ||
"unknown"
);
}
function filterHeaders(headers) {
const h = {};
const skip = new Set(["host", "connection", "x-cache", "x-ratelimit-limit", "x-ratelimit-remaining"]);
for (const [k, v] of Object.entries(headers)) {
if (skip.has(k.toLowerCase())) continue;
h[k] = v;
}
if (!h["accept-encoding"]) h["accept-encoding"] = "gzip, deflate, br";
return h;
}
function filterProxyHeaders(headers) {
const h = {};
const skip = new Set(["transfer-encoding", "connection", "keep-alive"]);
for (const [k, v] of Object.entries(headers)) {
if (skip.has(k.toLowerCase())) continue;
h[k] = v;
}
return h;
}
// ========== HTTP 服务器 ==========
const server = http.createServer((req, res) => {
const startTime = Date.now();
// 响应完成后记录日志
res.on("finish", () => {
const duration = Date.now() - startTime;
log("info", "Request completed", {
method: req.method,
url: req.url,
status: res.statusCode,
durationMs: duration,
ip: getClientIP(req),
});
});
if (req.method === "OPTIONS") {
res.writeHead(204, {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET,POST,PUT,DELETE,PATCH,OPTIONS",
"access-control-allow-headers": "*",
"access-control-max-age": "86400",
});
res.end();
return;
}
if (req.url === "/" || req.url === "") {
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end(`<!DOCTYPE html>
<html>
<head><title>HF Mirror</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; padding: 40px; max-width: 800px; margin: 0 auto; line-height: 1.6; }
h1 { color: #333; }
.stat { background: #f5f5f5; padding: 12px 16px; border-radius: 8px; margin: 8px 0; }
.stat-label { font-weight: 600; color: #666; }
.stat-value { color: #111; font-family: monospace; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
.links { margin-top: 24px; }
.links a { margin-right: 16px; }
.cdn-list { font-size: 12px; color: #666; margin-top: 8px; }
</style>
</head>
<body>
<h1>🤗 HF Mirror Proxy</h1>
<div class="stat"><span class="stat-label">上游:</span> <span class="stat-value">${CONFIG.UPSTREAM}</span></div>
<div class="stat"><span class="stat-label">缓存:</span> <span class="stat-value">${cache.stats.size} 条 / ${(cache.stats.bytes / 1024 / 1024).toFixed(2)} MB</span></div>
<div class="stat"><span class="stat-label">URL映射:</span> <span class="stat-value">${urlMap.stats.size} 条</span></div>
<div class="stat"><span class="stat-label">限流客户端:</span> <span class="stat-value">${rateLimiter.stats.clients} 个</span></div>
<div class="cdn-list">
外部CDN代理: ${Array.from(EXTERNAL_CDN_HOSTS).join(", ")} (及含 xethub/cas-bridge 的域名)
</div>
<div class="links">
<a href="/health">健康检查</a>
<a href="/cache/clear">清缓存</a>
<a href="/metrics">指标</a>
</div>
</body>
</html>`);
return;
}
if (req.url === "/health") {
res.writeHead(200, {
"content-type": "application/json",
"access-control-allow-origin": "*",
});
res.end(
JSON.stringify({
status: "ok",
upstream: CONFIG.UPSTREAM,
cache: cache.stats,
urlMap: urlMap.stats,
rateLimiter: rateLimiter.stats,
uptime: process.uptime(),
memory: process.memoryUsage(),
})
);
return;
}
if (req.url === "/metrics") {
res.writeHead(200, {
"content-type": "application/json",
"access-control-allow-origin": "*",
});
res.end(
JSON.stringify({
cache: cache.stats,
urlMap: urlMap.stats,
rateLimiter: rateLimiter.stats,
uptime: process.uptime(),
memory: {
heapUsed: `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`,
heapTotal: `${(process.memoryUsage().heapTotal / 1024 / 1024).toFixed(2)} MB`,
rss: `${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`,
external: `${(process.memoryUsage().external / 1024 / 1024).toFixed(2)} MB`,
},
})
);
return;
}
if (req.url === "/cache/clear") {
cache.clear();
urlMap.clear();
log("info", "Cache cleared manually");
res.writeHead(200, {
"content-type": "application/json",
"access-control-allow-origin": "*",
});
res.end(JSON.stringify({ cleared: true, timestamp: new Date().toISOString() }));
return;
}
proxyRequest(req, res);
});
server.listen(CONFIG.PORT, "0.0.0.0", () => {
console.log(`
🤗 HF Mirror Proxy 已启动
─────────────────────────────
→ 上游: ${CONFIG.UPSTREAM}
→ 端口: ${CONFIG.PORT}
→ 公网域名: ${CONFIG.PUBLIC_HOST || "(自动检测)"}
→ 日志级别: ${CONFIG.LOG_LEVEL}
→ 缓存上限: ${CONFIG.CACHE_MAX_SIZE} 条 / ${(CONFIG.CACHE_MAX_BYTES / 1024 / 1024).toFixed(0)} MB
→ 限流: ${CONFIG.RATE_LIMIT_MAX} 请求/${CONFIG.RATE_LIMIT_WINDOW / 1000}s
→ 外部CDN: ${Array.from(EXTERNAL_CDN_HOSTS).join(", ")}
→ 健康检查: http://localhost:${CONFIG.PORT}/health
→ 指标: http://localhost:${CONFIG.PORT}/metrics
→ 清缓存: http://localhost:${CONFIG.PORT}/cache/clear
`);
});
// 优雅关闭
process.on("SIGTERM", () => {
log("info", "Received SIGTERM, shutting down gracefully");
server.close(() => {
log("info", "Server closed");
process.exit(0);
});
});
process.on("SIGINT", () => {
log("info", "Received SIGINT, shutting down gracefully");
server.close(() => {
log("info", "Server closed");
process.exit(0);
});
});