天天看點

使用 Cloudflare Workers 解決 OpenAI 和 ChatGPT 的 API 無法通路的問題

事實證明 ChatGPT 是足夠火爆的,火爆到什麼程度呢,其 API 一經推出便獲得了 GFW 的認證。在 Twitter 上看到很多人都在為解決無法正常通路 OpenAI 的 API 而苦惱,最常見解決方案是使用一台伺服器來進行反向代理,但這樣又徒增了一些成本。因為之前在公司的業務上遇到過類似問題,當時老闆找到了一個還不錯的幾乎零成本解決方案,試了一下現在仍然可以用來解決 OpenAI 的 API 無法通路的問題,是以在這裡推薦給大家。

該方案的主要思路是使用 Cloudflare 的 Workers 來代理 OpenAI 的 API 位址,配合自己的域名即可在境内實作通路。因為 Cloudflare Workers 有每天免費 10 萬次的請求額度,也有可以免費注冊的域名,是以幾乎可以說是零成本。而且該方法理論上支援所有被認證的網站,而不隻是 OpenAI。

使用這個方案需要你有以下東西:

  • 一個沒有被 GFW 認證的域名,相信對于大家來說注冊域名不是啥大問題)
  • 一個 Cloudflare 賬号(當然也可以現注冊)

太長不看

  1. 建立一個 Cloudflare Worker
  2. 将 https://gist.github.com/noobnooc/d0407b5fb81cff9d36f981170b99d4e6 裡的代碼粘貼到 Worker 中并部署
  3. 給 Worker 綁定一個沒有被 GFW 認證的域名
  4. 使用自己的域名代替 api.openai.com

如果具體步驟有問題,可以參考下面的詳細版教程。

将域名 NS 轉到 Cloudflare

如果域名已經托管在 Cloudflare 的忽略這一步即可。

⚠️ 經評論區指出,Cloudflare Workers 的域名綁定僅支援托管在 Cloudflare 上的域名。由于本人常年是把域名托管在 Cloudflare 的沒有注意到這一點,是以得先将域名的 NS 轉到 Cloudflare,如果介意将域名轉到 Cloudflare 的話,可以考慮使用 nginx 反代、Docker 容器等其他方法 。

沒有 Cloudflare 賬号的話可以注冊一個,具體注冊細節就不多說了。注冊或登入到 Cloudflare 的管理界面後,點選側邊欄的 “Websites” ,然後點選 “Add a Site” 按鈕準備将域名轉到 Cloudflare:

建立一個 Cloudflare Worker

登入到 Cloudflare 的管理界面後,點選側邊欄的 “Workers” 選項,然後點選 “Create a Service” 建立一個 Worker。

使用 Cloudflare Workers 解決 OpenAI 和 ChatGPT 的 API 無法通路的問題

然後在建立界面中輸入 “Service name” 後點選 “Create Service” 按鈕建立 Worker。“Select a starter” 項先不用管。

使用 Cloudflare Workers 解決 OpenAI 和 ChatGPT 的 API 無法通路的問題

至此 Cloudflare 的 Worker 便建立好了,下面開始修改 Worker 的代碼,使其能代理 OpenAI 的 API。

修改 Cloudflare Worker 的代碼

在 Worker 的管理界面,點選右上角的 “Quick Edit” 按鈕編輯代碼 Worker 的代碼。

使用 Cloudflare Workers 解決 OpenAI 和 ChatGPT 的 API 無法通路的問題

在 “Enter your site (example.com)” 處輸入要轉入的域名後,點選 “Add Site”:

使用 Cloudflare Workers 解決 OpenAI 和 ChatGPT 的 API 無法通路的問題

根據 Cloudflare 的提示,在域名注冊商處将 NS 修改到 Cloudflare 指定的位址,等待域名解析成功後,即可進行後續操作。

在左側的代碼編輯器中,删除現有的所有代碼,然後複制粘貼以下内容到代碼編輯器:

// Website you intended to retrieve for users.
const upstream = 'api.openai.com'

// Custom pathname for the upstream website.
const upstream_path = '/'

// Website you intended to retrieve for users using mobile devices.
const upstream_mobile = upstream

// Countries and regions where you wish to suspend your service.
const blocked_region = []

// IP addresses which you wish to block from using your service.
const blocked_ip_address = ['0.0.0.0', '127.0.0.1']

// Whether to use HTTPS protocol for upstream address.
const https = true

// Whether to disable cache.
const disable_cache = false

// Replace texts.
const replace_dict = {
    '$upstream': '$custom_domain',
}

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

async function fetchAndApply(request) {
    const region = request.headers.get('cf-ipcountry').toUpperCase();
    const ip_address = request.headers.get('cf-connecting-ip');
    const user_agent = request.headers.get('user-agent');

    let response = null;
    let url = new URL(request.url);
    let url_hostname = url.hostname;

    if (https == true) {
        url.protocol = 'https:';
    } else {
        url.protocol = 'http:';
    }

    if (await device_status(user_agent)) {
        var upstream_domain = upstream;
    } else {
        var upstream_domain = upstream_mobile;
    }

    url.host = upstream_domain;
    if (url.pathname == '/') {
        url.pathname = upstream_path;
    } else {
        url.pathname = upstream_path + url.pathname;
    }

    if (blocked_region.includes(region)) {
        response = new Response('Access denied: WorkersProxy is not available in your region yet.', {
            status: 403
        });
    } else if (blocked_ip_address.includes(ip_address)) {
        response = new Response('Access denied: Your IP address is blocked by WorkersProxy.', {
            status: 403
        });
    } else {
        let method = request.method;
        let request_headers = request.headers;
        let new_request_headers = new Headers(request_headers);

        new_request_headers.set('Host', upstream_domain);
        new_request_headers.set('Referer', url.protocol + '//' + url_hostname);

        let original_response = await fetch(url.href, {
            method: method,
            headers: new_request_headers,
            body: request.body
        })

        connection_upgrade = new_request_headers.get("Upgrade");
        if (connection_upgrade && connection_upgrade.toLowerCase() == "websocket") {
            return original_response;
        }

        let original_response_clone = original_response.clone();
        let original_text = null;
        let response_headers = original_response.headers;
        let new_response_headers = new Headers(response_headers);
        let status = original_response.status;
		
		if (disable_cache) {
			new_response_headers.set('Cache-Control', 'no-store');
	    }

        new_response_headers.set('access-control-allow-origin', '*');
        new_response_headers.set('access-control-allow-credentials', true);
        new_response_headers.delete('content-security-policy');
        new_response_headers.delete('content-security-policy-report-only');
        new_response_headers.delete('clear-site-data');
		
		if (new_response_headers.get("x-pjax-url")) {
            new_response_headers.set("x-pjax-url", response_headers.get("x-pjax-url").replace("//" + upstream_domain, "//" + url_hostname));
        }
		
        const content_type = new_response_headers.get('content-type');
        if (content_type != null && content_type.includes('text/html') && content_type.includes('UTF-8')) {
            original_text = await replace_response_text(original_response_clone, upstream_domain, url_hostname);
        } else {
            original_text = original_response_clone.body
        }
		
        response = new Response(original_text, {
            status,
            headers: new_response_headers
        })
    }
    return response;
}

async function replace_response_text(response, upstream_domain, host_name) {
    let text = await response.text()

    var i, j;
    for (i in replace_dict) {
        j = replace_dict[i]
        if (i == '$upstream') {
            i = upstream_domain
        } else if (i == '$custom_domain') {
            i = host_name
        }

        if (j == '$upstream') {
            j = upstream_domain
        } else if (j == '$custom_domain') {
            j = host_name
        }

        let re = new RegExp(i, 'g')
        text = text.replace(re, j);
    }
    return text;
}


async function device_status(user_agent_info) {
    var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
    var flag = true;
    for (var v = 0; v < agents.length; v++) {
        if (user_agent_info.indexOf(agents[v]) > 0) {
            flag = false;
            break;
        }
    }
    return flag;
}           

最後點選編輯器右下角的 “Save and deploy” 按鈕部署該代碼,在彈出的對話框中繼續選擇 “Save and deploy” 确認部署。

使用 Cloudflare Workers 解決 OpenAI 和 ChatGPT 的 API 無法通路的問題

至此,便可以使用該 worker 的位址來代替 OpenAI 的 API 位址了。比如想要請求 ChatGPT 的 API 時,把官方文檔中的 https://api.openai.com/v1/chat/completions 替換成 https://openai.workers.dev 即可(注意這個位址并不存在,是需要換成自己剛剛建立的 Worker 的位址)。

但是你可能會發現,這樣做了依然還是沒有解決問題,因為 Cloudflare Workers 的 workers.dev 域名也是被 GFW 認證過的。但是好在隻是認證了 workers.dev 域名,而 ip 還是幸存的狀态,是以我們可以給 Worker 綁定一個自己的域名。

綁定域名

在 Cloudflare Workers 的管理界面中,點選 “Triggers” 頁籤,然後點選 “Custom Domians” 中的 “Add Custom Domain” 按鈕以綁定域名。

使用 Cloudflare Workers 解決 OpenAI 和 ChatGPT 的 API 無法通路的問題

輸入域名後點選 “Add Custom Domain”目前隻支援 NS 托管在 Cloudflare 上的域名,如果不介意,可以點選 Cloudflare 側邊欄的 “Websites”,然後點選 “Add a Site” 按鈕,根據提示将域名的 NS 記錄指定到 Cloudflare。

使用 Cloudflare Workers 解決 OpenAI 和 ChatGPT 的 API 無法通路的問題

至此便大功告成。等待片刻,應該就可以通過你自己的域名來代替 OpenAI 的 API 位址了,比如在本文的例子中,想要請求 ChatGPT 的 API ,即是把官方 API 位址 https://api.openai.com/v1/chat/completions 換為我自己的域名,其他參數均參照官方示例即可。由于 Cloudflare 有每天免費 10 萬次的請求額度,是以輕度使用基本是零成本的

繼續閱讀