杆子博客

杆子博客

博采众长 一诺千金!
当前位置: 首页 > 教程 > 正文

利用 Cloudflare Workers 反代并加速 Hugging Face Spaces (支持自定义域名)

在国内访问 Hugging Face Spaces 速度较慢,且免费账户无法绑定自定义域名。本文记录了如何利用 Cloudflare Workers 反向代理,配合 Cloudflare for SaaS 功能,实现自定义域名访问及线路优化(优选 IP)。

前置准备

  1. Hugging Face 项目:已部署好的 Space(建议 Docker 或静态应用)。
  2. Cloudflare 账号:拥有一个托管在 CF 的域名(作为回退源,例如 delln.us.ci)。
  3. 国内 DNS 账号:拥有一个想绑定的对外域名(例如阿里云/腾讯云托管的 zznuo.com)。

第一步:获取 Hugging Face 直链

  1. 进入你的 HF Space 页面。
  2. 点击右上角 ... -> Embed this space
  3. 复制 Direct URL
    • 格式通常为:https://用户名-项目名.hf.space
    • 注意:不要直接用浏览器地址栏的地址,那个带导航栏。

第二步:部署 Cloudflare Workers

  1. 在 Cloudflare 后台 -> Workers & Pages -> Create Application
  2. 创建一个新的 Worker,粘贴以下代码:
/**
 * 全能通用版 Hugging Face 反代 Worker
 * 功能:多域名映射 + WebSocket支持 + 跨域优化
 */

// 1. 配置你的域名映射表
// 左边是你绑定的自定义域名,右边是 HF 的直链
const UPSTREAM_MAP = {
  // 示例 1:n8n (需要 WebSocket)
  'n8n.zznuo.com': 'https://ganzihai-n8n.hf.space',
  'kk.zznuo.com': 'https://v52-kk.hf.space',
};

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  const domain = url.hostname;

  // 2. 查找当前域名对应的源站地址
  const upstream = UPSTREAM_MAP[domain];

  // 如果域名不在列表里,返回 404
  if (!upstream) {
    return new Response(`Error: Domain ${domain} is not configured in Worker mapping.`, {
      status: 404,
      headers: { 'content-type': 'text/plain;charset=UTF-8' }
    });
  }

  // 3. 构建新的请求 URL
  const upstreamUrl = new URL(upstream);
  const newUrl = new URL(upstream);
  newUrl.pathname = url.pathname;
  newUrl.search = url.search;

  // 4. 复制原始请求的 Header
  const newHeaders = new Headers(request.headers);
  
  // 关键:覆盖 Host 头部,让 HF 知道访问的是哪个 Space
  newHeaders.set('Host', upstreamUrl.hostname);
  
  // 补充 Referer 和 Origin (部分应用需要检查)
  newHeaders.set('Referer', upstream);
  
  // 检查是否是 WebSocket 连接
  const upgradeHeader = request.headers.get('Upgrade');
  const isWebsocket = upgradeHeader === 'websocket';

  // 5. 构建请求对象
  const newRequest = new Request(newUrl.toString(), {
    method: request.method,
    headers: newHeaders,
    body: request.body,
    redirect: 'manual'
  });

  try {
    const response = await fetch(newRequest);

    // 6. WebSocket 特殊处理
    // 如果源站返回 101 Switching Protocols,说明握手成功,直接返回 Response 对象
    if (response.status === 101) {
      return response;
    }

    // 7. 普通 HTTP 请求处理
    // 重建响应头,解决跨域问题 (CORS)
    const responseHeaders = new Headers(response.headers);
    responseHeaders.set('Access-Control-Allow-Origin', '*');
    responseHeaders.set('Access-Control-Allow-Methods', 'GET, HEAD, POST, PUT, DELETE, OPTIONS');
    responseHeaders.set('Access-Control-Allow-Headers', '*');

    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: responseHeaders
    });

  } catch (err) {
    return new Response('Proxy Error: ' + err.toString(), { status: 500 });
  }
}
  1. 点击 Deploy 部署。

第三步:配置回退源 (Fallback Origin) —— 关键步骤

为了使用 SaaS 功能(CNAME 接入),我们需要配置一个"回退源域名"。假设使用 pan.delln.us.ci

1. 添加虚拟 DNS 记录:

  • 进入 delln.us.ci 的 DNS 设置。
  • 添加 A 记录:
    • Name: pan
    • IPv4: 192.0.2.1 (这是个保留 IP,俗称黑洞 IP,用于占位)
    • Proxy Status: 必须开启 Proxied (橙色云朵)。

2. 设置 Worker 路由 (Triggers):

  • 进入刚才创建的 Worker -> Settings -> Triggers
  • 点击 Add Route (不要用 Custom Domains)。
  • Route1: pan.delln.us.ci/* (注意末尾的 /*)。
  • Route2: *.zznuo.com/* (注意末尾的 /*)。
  • Zone: 选择 delln.us.ci

原理:当请求到达 pan 子域名时,Worker 会拦截请求,因此流量不会真的去连 192.0.2.1

第四步:开启 Cloudflare for SaaS

进入 delln.us.ciSSL/TLS -> Custom Hostnames (自定义主机名)。

1. 设置回退源:

  • Fallback Origin: 输入 pan.delln.us.ci
  • 等待状态变为 Effective (有效)。

2. 添加自定义域名:

  • 点击 Add Custom Hostname
  • 输入你想对外的域名:fp.zznuo.com
  • 按照提示,去阿里云/腾讯云添加 TXT 记录完成所有权验证。

第五步:国内 DNS 解析 (完成)

最后,去你的域名注册商(如阿里云 DNS):

  • 记录类型:CNAME
  • 主机记录:fp
  • 记录值:pan.delln.us.ci

进阶加速 (优选 IP):如果你想优化国内访问速度,可以等将上面的 CNAME 记录改为 Cloudflare 优选域名。

此处注意:必须等自定义主机完成主机验证和证书验证后,才能将 CNAME 记录改为 Cloudflare 优选域名。

第六步:部署 Cloudflare Workers——应对 Cloudflare 不定时主机验证和证书续期

  1. 在 Cloudflare 后台 -> Workers & Pages -> Create Application
  2. 创建一个新的 Worker,粘贴以下代码:
/**
 * Cloudflare SaaS 全自动双向切换脚本
 * 逻辑:
 * 1. 异常救援:当 CF 状态异常或证书异常时 -> 自动切回 [回退源] 救命。
 * 2. 自动恢复:当 CF 状态完全恢复(Active) 且 当前 DNS 是回退源时 -> 自动切回 [优选域名]。
 */

// ================= 配置区域 =================

const TARGET_DOMAINS = [
  // 格式:{ domain: "完整域名", rr: "主机头", baseDomain: "一级域名" }
  { domain: "n8n.zznuo.com", rr: "n8n", baseDomain: "zznuo.com" },
  { domain: "kk.zznuo.com", rr: "kk", baseDomain: "zznuo.com" },
];

// 1. 官方回退源地址 (救命用,用于通过验证)
const FALLBACK_ORIGIN = "pan.delln.us.ci"; 

// 2. 你的优选域名/优选IP CNAME (平时用,加速用)
const PREFERRED_ORIGIN = "yg9.ygkkk.dpdns.org"; 

// ===========================================

export default {
  async scheduled(event, env, ctx) {
    await processDomains(env);
  },

  async fetch(request, env, ctx) {
    const result = await processDomains(env);
    return new Response(JSON.stringify(result, null, 2), { 
      headers: { "content-type": "application/json;charset=UTF-8" } 
    });
  },
};

async function processDomains(env) {
  const results = [];
  for (const item of TARGET_DOMAINS) {
    const status = await checkAndSwitch(env, item);
    results.push(status);
  }
  return results;
}

async function checkAndSwitch(env, item) {
  const { domain, rr, baseDomain } = item;
  
  // 1. 获取 Cloudflare 状态
  const cfStatus = await getCloudflareStatus(env, domain);
  if (!cfStatus.success) return { domain, error: "CF API Failed" };
  
  // 2. 获取阿里云当前 DNS 记录
  const aliRecord = await getAliyunRecord(env, rr, baseDomain);
  if (!aliRecord.success) return { domain, error: "Aliyun API Failed: " + aliRecord.msg };
  
  const currentDNS = aliRecord.value; // 当前阿里云上的 CNAME 值
  const isHealthy = cfStatus.status === 'active' && cfStatus.ssl === 'active';
  
  // 归一化域名比较 (移除末尾的点,转小写)
  const normCurrent = currentDNS.replace(/\.$/, "").toLowerCase();
  const normFallback = FALLBACK_ORIGIN.replace(/\.$/, "").toLowerCase();
  const normPreferred = PREFERRED_ORIGIN.replace(/\.$/, "").toLowerCase();

  let action = "None";
  let msg = "状态保持不变";

  // ================= 核心判断逻辑 =================

  if (!isHealthy) {
    // --- 场景 A:状态异常 (需要救援) ---
    // 如果当前不是回退源,就必须切回回退源
    if (normCurrent !== normFallback) {
      const update = await updateAliyunDNS(env, aliRecord.recordId, rr, FALLBACK_ORIGIN);
      if (update.success) {
        action = "Rescue";
        msg = `⚠️ 检测到异常 (${cfStatus.ssl}/${cfStatus.status}),已切回回退源救命。`;
        await sendBark(env, "暂停域名加速,待恢复...", `${domain}\n${msg}`);
      } else {
        msg = "尝试切回回退源失败: " + update.msg;
      }
    } else {
      msg = `状态异常,但当前已在回退源,等待证书恢复中... (${cfStatus.ssl})`;
    }
  } else {
    // --- 场景 B:状态健康 (尝试恢复优选) ---
    // 如果当前是回退源,说明之前可能坏过,现在好了,可以切回优选了
    // 注意:只有当前指向回退源时才切。如果用户手动指了别的IP,脚本不动,防止冲突。
    if (normCurrent === normFallback) {
      const update = await updateAliyunDNS(env, aliRecord.recordId, rr, PREFERRED_ORIGIN);
      if (update.success) {
        action = "Restore";
        msg = `✅ 状态已恢复 (Active),自动切回优选域名加速。`;
        await sendBark(env, "SaaS 恢复模式启动", `${domain}\n${msg}`);
      } else {
        msg = "尝试切回优选失败: " + update.msg;
      }
    } else if (normCurrent === normPreferred) {
        msg = "状态健康,当前已是优选域名。";
    } else {
        msg = "状态健康,当前为第三方值(非回退/非优选),脚本跳过。";
    }
  }

  return { domain, action, msg, current_dns: currentDNS, cf_status: cfStatus };
}

// ================= 辅助函数区域 =================

// 获取 CF 状态
async function getCloudflareStatus(env, domain) {
  const url = `https://api.cloudflare.com/client/v4/zones/${env.CF_ZONE_ID}/custom_hostnames?hostname=${domain}`;
  try {
    const resp = await fetch(url, {
      headers: { "Authorization": `Bearer ${env.CF_API_TOKEN}`, "Content-Type": "application/json" }
    });
    const data = await resp.json();
    if (!data.success || data.result.length === 0) return { success: false };
    return { 
      success: true, 
      status: data.result[0].status, 
      ssl: data.result[0].ssl?.status 
    };
  } catch (e) { return { success: false }; }
}

// 获取阿里云记录
async function getAliyunRecord(env, rr, baseDomain) {
  try {
    const res = await aliyunRequest(env, {
      Action: "DescribeDomainRecords",
      DomainName: baseDomain,
      RRKeyWord: rr, 
      Type: "CNAME"
    });
    const records = res.DomainRecords?.Record || [];
    const target = records.find(r => r.RR === rr);
    if (!target) return { success: false, msg: "Record Not Found" };
    return { success: true, value: target.Value, recordId: target.RecordId };
  } catch (e) { return { success: false, msg: e.message }; }
}

// 更新阿里云记录
async function updateAliyunDNS(env, recordId, rr, value) {
  try {
    await aliyunRequest(env, {
      Action: "UpdateDomainRecord",
      RecordId: recordId,
      RR: rr,
      Type: "CNAME",
      Value: value
    });
    return { success: true };
  } catch (e) { return { success: false, msg: e.message }; }
}

// 阿里云通用请求 (含签名算法)
async function aliyunRequest(env, params) {
  const publicParams = {
    Format: "JSON", Version: "2015-01-09", AccessKeyId: env.ALI_KEY_ID,
    SignatureMethod: "HMAC-SHA1", Timestamp: new Date().toISOString(),
    SignatureVersion: "1.0", SignatureNonce: Math.random() + Date.now(),
  };
  const allParams = { ...publicParams, ...params };
  const sortedKeys = Object.keys(allParams).sort();
  const qs = sortedKeys.map(key => percentEncode(key) + "=" + percentEncode(allParams[key])).join("&");
  const stringToSign = "GET&%2F&" + percentEncode(qs);
  const signature = await computeSignature(stringToSign, env.ALI_KEY_SECRET + "&");
  const url = `https://alidns.aliyuncs.com/?${qs}&Signature=${percentEncode(signature)}`;
  
  const resp = await fetch(url);
  const data = await resp.json();
  if (resp.status !== 200) throw new Error(data.Message);
  return data;
}

function percentEncode(str) {
  return encodeURIComponent(str).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
}

async function computeSignature(stringToSign, secret) {
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
  const signature = await crypto.subtle.sign("HMAC", key, enc.encode(stringToSign));
  return btoa(String.fromCharCode(...new Uint8Array(signature)));
}

async function sendBark(env, title, body) {
  if (!env.BARK_URL) return;
  let url = env.BARK_URL.endsWith("/") ? env.BARK_URL : env.BARK_URL + "/";
  await fetch(`${url}${encodeURIComponent(title)}/${encodeURIComponent(body)}`);
}
  1. 点击 Deploy 部署,注意脚本要设置变量:ALI_KEY_IDALI_KEY_SECRETBARK_URLCF_API_TOKENCF_ZONE_ID
  2. 在设置——触发事件中添加定时任务:0 0-16 * * *

验证

访问 https://fp.zznuo.com。如果看到 Hugging Face 的界面,说明配置成功!

注意:如果 Hugging Face Space 处于休眠状态 (Sleep),初次访问可能需要等待几十秒用于容器冷启动,也可自行配置保活。

第七步:部署 Cloudflare Workers——Hugging Face 保活

  1. 在 Cloudflare 后台 -> Workers & Pages -> Create Application
  2. 创建一个新的 Worker,粘贴以下代码:
export default {
  // 1. 浏览器访问触发
  async fetch(request, env, ctx) {
    const report = await doKeepAlive();
    return new Response(report, {
      headers: { "content-type": "text/plain; charset=utf-8" },
    });
  },

  // 2. 定时任务触发
  async scheduled(event, env, ctx) {
    const report = await doKeepAlive();
    console.log(report);
  },
};

// --- 核心保活逻辑 ---
async function doKeepAlive() {
  // ⚠️ 注意:每个网址都要用引号包起来,并且行尾必须有逗号
  const urls = [
    "https://ganzihai-n8n.hf.space",
    "https://v52-kk.hf.space"
  ];

  const results = await Promise.all(
    urls.map(async (url) => {
      try {
        // 设置 5 秒超时,防止 Worker 卡死
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 5000);

        const response = await fetch(url, { 
            signal: controller.signal,
            headers: { 
                'User-Agent': 'Cloudflare-Worker-KeepAlive' 
            }
        });
        
        clearTimeout(timeoutId);

        if (response.status === 200) {
          return `✅ 成功: ${url} (200 OK)`;
        } else {
          return `⚠️ 异常: ${url} (Status: ${response.status})`;
        }
      } catch (error) {
        return `❌ 失败: ${url} (Error: ${error.message})`;
      }
    })
  );

  const finalReport = `[${new Date().toISOString()}] 保活检查报告:\n` + results.join("\n");
  return finalReport;
}

点击 Deploy 部署并设置定时触发。

结束:至此,整个教程结束。

打赏支持
支付宝打赏 支付宝打赏
微信打赏 微信打赏

「请 GANZI 喝杯咖啡作为鼓励」~

您可能还会对这些文章感兴趣!

导航
侧边栏