【小白教程】优雅的反代oaifree和分享gpt账号

本教程是对始皇,R佬(Reno)相关教程的整理,方便其余佬友们食用。

适用场景

  • 手里有多个gpt账号(普号 or plus)
  • 想要分享给多个无网络基础的朋友(有网络基础直接邀请进来玩shared)

前提条件

  • cloudflare账号一枚
  • 掌握cf worker创建和自定义域名路由配置

准备工作

总共需要创建3个worker,假如佬友自己的cf域名为“xxx.com”,后续步骤中涉及的需要替换成自己实际的域名地址,下面为创建的样例地址

反代voice.oaifree.com

直接创新worker即可,自定义域名可以根据自己情况修改

voice.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    url.host = 'voice.oaifree.com';
    return fetch(new Request(url, request));
  }
}

反代 new.oaifree.com

此处稍微改动了下始皇的反代脚本,主要解决token过期时跳转到at登录界面和gpt账号主动注销跳转到gpt官网登录界面的问题,统一跳转到上面的第三个worker地址“gpt.xxx.com”;需要将下面“gpt.xxx.com”和“voice.xxx.com”修改成自己实际的地址。

chat.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    url.host = 'new.oaifree.com';
    if(url.pathname === "/auth/login_auth0" || url.pathname === "/auth/login"){
      return Response.redirect("https://gpt.xxx.com/", 301);
    }

    const modifiedRequest = new Request(url, request);
    modifiedRequest.headers.set('X-Voice-Base', 'https://voice.xxx.com');

    return fetch(modifiedRequest);
  }
}

gpt账号管理和登录

1、新建 Turnstile,名字随意,选择对应的域名,新建完成后会得到“站点密钥”,记录下来后面cf验证需要。

2、新建KV存储,名字“oaifree”,能够区分就行


3、添加环境变量
ALLOWED_USERS:登录用户,多个用户用英文逗号隔开
SITE_PASSWORD:统一登录密码,所有用户登录都是这个密码
TURNSTILE_SITE_KEY:第1步中的“站点密钥”
YOUR_DOMAIN:反代new的自定义域名地址“chat.xxx.com

剩下的就是账号,KEY是账号名称,中间必须带@符号,可以直接用gpt邮箱,VALUE是一个json串:

{"refresh_token":"aaa","access_token":"bbb"}

如果没有refresh_token那么需要手动定期刷新下access_token

4、新建gpt账号管理worker

gpt.js
addEventListener("fetch", event => {
    event.respondWith(handleRequest(event.request));
});

// 主请求处理函数
async function handleRequest(request) {
    if (request.method === "POST") {
        const url = new URL(request.url);
        if (url.pathname === "/accounts") {
            return handleAccountSelection(request);
        } else {
            return handleFormSubmission(request);
        }
    } else {
        return displayLoginPage();
    }
}

// 处理用户提交的表单
async function handleFormSubmission(request) {
    const formData = await request.formData();
    try {
        const unique_name = await validateUser(formData);
        return displayAccountPage(unique_name);
    } catch (error) {
        return displayErrorPage(error.message);
    }
}

// 处理账户选择
async function handleAccountSelection(request) {
    const formData = await request.formData();
    const unique_name = formData.get("unique_name");
    const account_key = formData.get("account_key");

    try {
        const accountData = JSON.parse(await KV.get(account_key));
        const access_token = accountData.access_token || await getValidToken(account_key);
        const shareToken = await generateShareToken(unique_name, access_token);
        const YOUR_DOMAIN = (await KV.get("YOUR_DOMAIN")) || new URL(request.url).host;
        const oauthLink = await getOAuthLink(shareToken, YOUR_DOMAIN);
        return Response.redirect(oauthLink, 302);
    } catch (error) {
        return displayErrorPage(error.message);
    }
}

// 验证用户身份
async function validateUser(formData) {
    const SITE_PASSWORD = (await KV.get("SITE_PASSWORD")) || "";
    const ALLOWED_USERS = (await KV.get("ALLOWED_USERS")) || "";
    const site_password = formData.get("site_password") || "";
    const unique_name = formData.get("unique_name") || "";

    if (site_password !== SITE_PASSWORD) {
        throw new Error("访问密码错误");
    }

    const allowedUsersArray = ALLOWED_USERS.split(",");
    if (!allowedUsersArray.includes(unique_name)) {
        throw new Error("用户不在白名单中");
    }

    return unique_name;
}

// 获取所有账户的键
async function getAccounts() {
    const allKeys = await KV.list(); // 返回所有键的数组
    return allKeys.keys.filter(key => key.name.includes("@")).map(key => key.name);
}

// 获取有效令牌
async function getValidToken(account_key) {
    let accountData = JSON.parse(await KV.get(account_key));
    let token = accountData.access_token;
    if (!token || isTokenExpired(token)) {
        token = await refreshToken(account_key);
    }
    return token;
}

// 刷新 Token
async function refreshToken(account_key) {
    const url = "https://token.oaifree.com/api/auth/refresh";
    let accountData = JSON.parse(await KV.get(account_key));
    const refreshToken = accountData.refresh_token;
    const response = await fetch(url, {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
        },
        body: `refresh_token=${refreshToken}`,
    });
    if (!response.ok) throw new Error("Error fetching access token");
    const data = await response.json();
    accountData.access_token = data.access_token;
    await KV.put(account_key, JSON.stringify(accountData));
    return data.access_token;
}

// 生成共享令牌
async function generateShareToken(unique_name, access_token) {
    const url = "https://chat.oaifree.com/token/register";
    const body = new URLSearchParams({
        unique_name,
        access_token,
        site_limit: "",
        expires_in: "0",
        gpt35_limit: "-1",
        gpt4_limit: "-1",
        show_conversations: "true",
        temporary_chat: "false",
        reset_limit: "false",
    });

    const response = await fetch(url, {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
        },
        body: body.toString(),
    });

    const data = await response.json();
    return data.token_key || "未找到 Share_token";
}

// 获取 OAuth 链接
async function getOAuthLink(shareToken, proxiedDomain) {
    const url = `https://${proxiedDomain}/api/auth/oauth_token`;
    const response = await fetch(url, {
        method: "POST",
        headers: {
            Origin: `https://${proxiedDomain}`,
            "Content-Type": "application/json",
        },
        body: JSON.stringify({ share_token: shareToken }),
    });
    const data = await response.json();
    return data.login_url;
}

// 解析 JWT Token
function isTokenExpired(token) {
    const payload = parseJwt(token);
    return payload.exp < Math.floor(Date.now() / 1000);
}

function parseJwt(token) {
    const base64Url = token.split(".")[1];
    const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
    const jsonPayload = decodeURIComponent(
        atob(base64)
            .split("")
            .map(c => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
            .join("")
    );
    return JSON.parse(jsonPayload);
}

// 显示登录页面
async function displayLoginPage() {
    const TURNSTILE_SITE_KEY = await KV.get("TURNSTILE_SITE_KEY");
    const formHtml = `
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <title>欢迎使用oaifree</title>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
            <link rel="icon" type="image/png" href="https://img.pub/p/8efdf03e4c4a5f057ac6.jpg">
            <style>
                body { background-image: url('https://image.daosuijun.top/file/0070dc8827c70b14ba67e.png'); background-size: cover; background-position: center; background-attachment: fixed; }
                input::placeholder { color: transparent; }
                input:not(:placeholder-shown) + label, input:focus + label { top: -20px; padding: 0 5px; left: 12px; z-index: 10; background: rgba(255, 255, 255, 0); }
                .container { background: rgba(255, 255, 255, 0.85); border-radius: 15px; padding: 30px; max-width: 400px; margin: auto; text-align: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }
                .logo { margin: 0 auto 20px; width: 120px; height: auto; }
                .title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 15px; }
                .form { margin-top: 20px; }
                .input-field { width: 100%; height: 45px; padding: 10px; border-radius: 5px; border: 1px solid #ccc; margin-bottom: 20px; }
                .button { background-color: #48bb78; color: white; padding: 12px 20px; border-radius: 5px; text-decoration: none; display: inline-block; transition: background-color 0.3s; width: 100%; }
                .button:hover { background-color: #38a169; }
            </style>
        </head>
        <body class="flex justify-center items-center min-h-screen m-0 bg-black bg-opacity-60">
            <div class="container">
                <img src="https://img.pub/p/dd92c1e0a8b081befd3d.jpg" alt="LINUX DO" class="logo">
                <h1 class="title">欢迎使用</h1>
                <form method="POST" class="form">
                    <div class="relative mb-4">
                        <input type="text" id="unique_name" name="unique_name" placeholder=" " required class="input-field">
                        <label for="unique_name" class="absolute left-5 top-1 text-green-500 text-sm transition-all duration-300">用户名</label>
                    </div>
                    <div class="relative mb-4">
                        <input type="password" id="site_password" name="site_password" placeholder=" " class="input-field">
                        <label for="site_password" class="absolute left-5 top-1 text-green-500 text-sm transition-all duration-300">口令</label>
                    </div>
                    <div class="cf-turnstile my-4 flex justify-center" data-sitekey="${TURNSTILE_SITE_KEY}" data-callback="onTurnstileCallback"></div>
                    <input type="hidden" id="cf-turnstile-response" name="cf-turnstile-response" required>
                    <button type="submit" class="w-full h-12 bg-green-500 hover:bg-green-600 text-white font-bold rounded-lg transition-colors duration-300">点击登录</button>
                </form>
            </div>
        </body>
        <script>
            function onTurnstileCallback(token) {
                document.getElementById('cf-turnstile-response').value = token;
            }

            document.querySelector('form').addEventListener('submit', function(event) {
                if (!document.getElementById('cf-turnstile-response').value) {
                    alert('请完成验证。');
                    event.preventDefault();
                }
            });
        </script>
        <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
        </html>
    `;
    return new Response(formHtml, {
        headers: {
            "Content-Type": "text/html; charset=utf-8",
        },
    });
}

// 显示账户选择页面
async function displayAccountPage(unique_name) {
    const keys = await getAccounts();
    const accountOptions = keys.map(key => `<option value="${key}">${key}</option>`).join("");
    const formHtml = `
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <title>选择账户</title>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
            <link rel="icon" type="image/png" href="https://img.pub/p/8efdf03e4c4a5f057ac6.jpg">
            <style>
                body { background-image: url('https://image.daosuijun.top/file/0070dc8827c70b14ba67e.png'); background-size: cover; background-position: center; background-attachment: fixed; }
                .container { background: rgba(255, 255, 255, 0.85); border-radius: 15px; padding: 30px; max-width: 400px; margin: auto; text-align: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }
                .logo { margin: 0 auto 20px; width: 120px; height: auto; }
                .title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 15px; }
                .form { margin-top: 20px; }
                .select { width: 100%; height: 45px; padding: 10px; border-radius: 5px; border: 1px solid #ccc; margin-bottom: 20px; }
                .button { background-color: #48bb78; color: white; padding: 12px 20px; border-radius: 5px; text-decoration: none; display: inline-block; transition: background-color 0.3s; width: 100%; }
                .button:hover { background-color: #38a169; }
            </style>
        </head>
        <body class="flex justify-center items-center min-h-screen m-0 bg-black bg-opacity-60">
            <div class="container">
                <img src="https://img.pub/p/dd92c1e0a8b081befd3d.jpg" alt="LINUX DO" class="logo">
                <h1 class="title">选择账户</h1>
                <form method="POST" action="/accounts" class="form">
                    <input type="hidden" name="unique_name" value="${unique_name}">
                    <select name="account_key" class="select">
                        ${accountOptions}
                    </select>
                    <button type="submit" class="w-full h-12 bg-green-500 hover:bg-green-600 text-white font-bold rounded-lg transition-colors duration-300">开始使用</button>
                </form>
            </div>
        </body>
        </html>
    `;
    return new Response(formHtml, {
        headers: {
            "Content-Type": "text/html; charset=utf-8",
        },
    });
}

// 显示错误页面
function displayErrorPage(errorMessage) {
    const htmlContent = `
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <title>登录错误</title>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
            <link rel="icon" type="image/png" href="https://img.pub/p/8efdf03e4c4a5f057ac6.jpg">
            <style>
                body { background-image: url('https://image.daosuijun.top/file/0070dc8827c70b14ba67e.png'); background-size: cover; background-position: center; background-attachment: fixed; }
                .container { background: rgba(255, 255, 255, 0.85); border-radius: 15px; padding: 30px; max-width: 400px; margin: auto; text-align: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }
                .logo { margin: 0 auto 20px; width: 120px; height: auto; }
                .title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 15px; }
                .message { font-size: 18px; color: #e53e3e; margin-bottom: 25px; }
                .button { background-color: #48bb78; color: white; padding: 12px 20px; border-radius: 5px; text-decoration: none; display: inline-block; transition: background-color 0.3s; }
                .button:hover { background-color: #38a169; }
            </style>
        </head>
        <body class="flex justify-center items-center min-h-screen m-0 bg-black bg-opacity-60">
            <div class="container">
                <img src="https://img.pub/p/dd92c1e0a8b081befd3d.jpg" alt="LINUX DO" class="logo">
                <h1 class="title">登录错误</h1>
                <p class="message">${errorMessage}</p>
            </div>
        </body>
        </html>
    `;
    return new Response(htmlContent, {
        status: 401,
        headers: { "Content-Type": "text/html; charset=utf-8" },
    });
}

5、gpt对应的worker关联自定义域名和配置路由后,还需要绑定下KV命名空间,名称为“KV”,命名空间选前面创建的

最终效果

1、将地址 “https://chat.xxx.com/” 分享给小伙伴即可;如果未登录或者token失效会自动跳转到登录页面,如果已经登录则直接继续上次的对话聊天。
image
image

参考教程

1、感谢始皇提供的黑科技和教程

2、感谢R佬(Reno)提供的gpt账号管理和登录脚本

51 Likes

感谢大佬投喂

1 Like

前排

1 Like

这个教程很到位,适合小白上手

1 Like

好厉害,好优雅

3 Likes

让大帅哥见笑了

1 Like

条理清晰,好教程,回头来试试

感恩佬

真好,mark

1 Like

正在跟着大佬学,原来我只在new.oaifree获取过access_token。
请问,这个里面的refresh_token和access_token在哪里获取啊?

2 Likes

三级可以获取 refresh toke 和 access token,获取一次就够了

1 Like

啊,这样啊,我还是二级,那就用不了了

1 Like

用 access token 也可以,定期手动刷一次

1 Like

好的,那个refresh_token就留空吧,谢谢解答 :grinning:

前排询问 能隐藏gpt账号的昵称 头像 邮箱吗

对,随便填点啥都行,优先看 access token 有没有过期,没过期就直接用,过期了就会尝试用 refresh token 生成一个新的 access token

大佬这个问题估计得始皇出手,搞 oaifree 镜像站的时候单独屏蔽下这几个点

我目前用的这个佬的是能隐藏的

这个有点强,回头研究下看看怎么搞的

image
登录错误了,不知道什么原因