解决 OpenAI 和 ChatGPT 的 API 无法访问的问题

来自: https://github.com/noobnooc/noobnooc/discussions/9

事实证明 ChatGPT 是足够火爆的,火爆到什么程度呢,其 API 一经推出便获得了 GFW 的认证。在 Twitter 上看到很多人都在为解决无法正常访问 OpenAI 的 API 而苦恼,最常见解决方案是使用一台服务器来进行反向代理,但这样又徒增了一些成本。因为之前在公司的业务上遇到过类似问题,当时老板找到了一个还不错的几乎零成本解决方案,试了一下现在仍然可以用来解决 OpenAI 的 API 无法访问的问题,所以在这里推荐给大家。

该方案的主要思路是使用 Cloudflare 的 Workers 来代理 OpenAI 的 API 地址,配合自己的域名即可在境内实现访问。因为 Cloudflare Workers 有每天免费 10 万次的请求额度,也有可以免费注册的域名,所以几乎可以说是零成本。而且该方法理论上支持所有被认证的网站,而不只是 OpenAI。

使用这个方案需要你有以下东西:

  • 一个没有被 GFW 认证的域名(没有的话也可以到 https://www.freenom.com 免费注册一个,26a0.png
    据推友提醒,freenom 已暂停新用户注册,但相信对于大家来说注册域名不是啥大问题)
  • 一个 Cloudflare 账号(当然也可以现注册)

26a0.png
请不要直接使用本教程示例中的地址,因为随时会被关闭。也不要使用任何其他人搭建的不受信任的地址,因为有 api key 被盗取的可能。

太长不看

  1. 新建一个 Cloudflare Worker
  2.  https://gist.github.com/noobnooc/d0407b5fb81cff9d36f981170b99d4e6 里的代码粘贴到 Worker 中并部署
  3. 给 Worker 绑定一个没有被 GFW 认证的域名
  4. 使用自己的域名代替 api.openai.com

如果具体步骤有问题,可以参考下面的详细版教程。

1f195.png
将域名 NS 转到 Cloudflare

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

26a0.png
经评论区指出,Cloudflare Workers 的域名绑定仅支持托管在 Cloudflare 上的域名。由于本人常年是把域名托管在 Cloudflare 的没有注意到这一点,所以得先将域名的 NS 转到 Cloudflare,如果介意将域名转到 Cloudflare 的话,可以考虑使用 nginx 反代、Docker 容器等其他方法1f972.png

没有 Cloudflare 账号的话可以注册一个,具体注册细节就不多说了。注册或登录到 Cloudflare 的管理界面后,点击侧边栏的 “Websites” ,然后点击 “Add a Site” 按钮准备将域名转到 Cloudflare:

222916699-9f076d54-4f5b-4420-9d28-c3575d83c6a0.png

在 “Enter your site (example.com)” 处输入要转入的域名后,点击 “Add Site”:

222916797-637a6fff-8331-423a-bbff-13183dcd041a.png

根据 Cloudflare 的提示,在域名注册商处将 NS 修改到 Cloudflare 指定的地址,等待域名解析成功后,即可进行后续操作。

创建一个 Cloudflare Worker

登录到 Cloudflare 的管理界面后,点击侧边栏的 “Workers” 选项,然后点击 “Create a Service” 创建一个 Worker。

222871270-d973e2bd-74f9-4efa-b080-e665903a7bac.png

然后在创建界面中输入 “Service name” 后点击 “Create Service” 按钮新建 Worker。“Select a starter” 项先不用管。

222871439-4ac253c6-d87f-434e-a8e8-3d05c652217d.png

至此 Cloudflare 的 Worker 便创建好了,下面开始修改 Worker 的代码,使其能代理 OpenAI 的 API。

修改 Cloudflare Worker 的代码

在 Worker 的管理界面,点击右上角的 “Quick Edit” 按钮编辑代码 Worker 的代码。

222871710-c1d4834e-3bb4-49cf-a9dd-b7dcee258112.png

在左侧的代码编辑器中,删除现有的所有代码,然后复制粘贴以下内容到代码编辑器:

// 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” 确认部署。

222872146-bf4c8f30-307a-4511-9be6-8c19db5cab43.png

至此,便可以使用该 worker 的地址来代替 OpenAI 的 API 地址了。比如想要请求 ChatGPT 的 API 时,把官方文档中的 https://api.openai.com/v1/chat/completions 替换成 https://openai.workers.dev 即可(注意这个地址并不存在,是需要换成自己刚刚创建的 Worker 的地址)。

但是你可能会发现,这样做了依然还是没有解决问题,因为 Cloudflare Workers 的 workers.dev 域名也是被 GFW 认证过的1f972.png
。但是好在只是认证了 workers.dev 域名,而 ip 还是幸存的状态,所以我们可以给 Worker 绑定一个自己的域名。

绑定域名

在 Cloudflare Workers 的管理界面中,点击 “Triggers” 选项卡,然后点击 “Custom Domians” 中的 “Add Custom Domain” 按钮以绑定域名。

222872491-026600f5-c04c-49c5-9fdf-4d4abb0b66f7.png

输入域名后点击 “Add Custom Domain”,根据提示修改域名的 DNS 记录。因为我的域名是托管在 Cloudflare 上的,所以无需手动更改 DNS 记录,如果域名没有托管在 Cloudfalre 上,可以根据相关提示自行配置。26a0.png
据评论区提示,目前只支持 NS 托管在 Cloudflare 上的域名,如果不介意,可以点击 Cloudflare 侧边栏的 “Websites”,然后点击 “Add a Site” 按钮,根据提示将域名的 NS 记录指定到 Cloudflare。

222872581-b99c8870-bcc7-4b8d-ba1e-1ad7830ce776.png

至此便大功告成。等待片刻,应该就可以通过你自己的域名来代替 OpenAI 的 API 地址了,比如在本文的例子中,想要请求 ChatGPT 的 API ,即是把官方 API 地址 https://api.openai.com/v1/chat/completions 换为我自己的域名 https://openai.nooc.ink/v1/chat/completions ,其他参数均参照官方示例即可。由于 Cloudflare 有每天免费 10 万次的请求额度,所以轻度使用基本是零成本的。26a0.png
注意请不要使用我这里的 openai.nooc.ink,因为随时可能会被我关闭1f92a.png

222873100-8979dd0e-9572-4d6b-92d6-86d012bdd188.png


https://github.com/noobnooc/noobnooc/discussions/9

没有评论:

发表评论