利用 Oaifree 自建共享站

Reno 于 2025-07-06 发布

该贡献站服务由三个worker组成

语音

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

镜像

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://主程序自定义域/", 301);
    }

    const modifiedRequest = new Request(url, request);
    modifiedRequest.headers.set('X-Voice-Base', 'https://语音自定义域');

    return fetch(modifiedRequest);
  }
}

主程序

需要绑定KV,绑定名称 KV,KV配置见后文

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

// 在文件开头添加加密相关函数
async function encryptData(data) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const key = await crypto.subtle.importKey(
        "raw",
        encoder.encode(await KV.get("COOKIE_SECRET")),
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["sign"]
    );
    const signature = await crypto.subtle.sign("HMAC", key, dataBuffer);
    return btoa(JSON.stringify({
        data: btoa(data),
        signature: Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join('')
    }));
}

async function decryptData(encrypted) {
    try {
        const { data, signature } = JSON.parse(atob(encrypted));
        const originalData = atob(data);
        const encoder = new TextEncoder();
        const dataBuffer = encoder.encode(originalData);
        const key = await crypto.subtle.importKey(
            "raw",
            encoder.encode(await KV.get("COOKIE_SECRET")),
            { name: "HMAC", hash: "SHA-256" },
            false,
            ["verify"]
        );
        const signatureArray = new Uint8Array(signature.match(/.{2}/g).map(byte => parseInt(byte, 16)));
        const isValid = await crypto.subtle.verify("HMAC", key, signatureArray, dataBuffer);
        return isValid ? originalData : null;
    } catch (e) {
        return null;
    }
}

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

// 修改处理登录页面的函数
async function handleLoginPage(request) {
    const cookie = request.headers.get('Cookie') || '';
    const userCookie = cookie.split(';').find(c => c.trim().startsWith('user='));
    
    if (userCookie) {
        const encryptedData = userCookie.split('=')[1].trim();
        const decryptedData = await decryptData(encryptedData);
        
        if (decryptedData) {
            try {
                const userData = JSON.parse(decryptedData);
                const user = await validateUserWithoutPassword(userData.unique_name);
                if (user) {
                    // 直接复用 handleFormSubmission 中的账户获取逻辑
                    const groupList = user.group;
                    const itemList = await getGroupAccounts(groupList);
                    user.account = {
                        ...user.account,
                        ...itemList
                    };
                    return displayAccountPage(user);
                }
            } catch (error) {
                // Cookie 无效,继续显示登录页面
            }
        }
    }
    
    return displayLoginPage();
}

// 修改免密码验证函数
async function validateUserWithoutPassword(unique_name) {
    const userJson = await KV.get(`${unique_name}`);
    if (!userJson) {
        return null;
    }
    const user = JSON.parse(userJson);
    return user;
}

// 修改表单提交处理函数
async function handleFormSubmission(request) {
    const formData = await request.formData();
    try {
        const user = await validateUser(formData);
        const groupList = user.group;
        const accounts = await getGroupAccounts(groupList);
        user.account = {
            ...user.account,
            ...accounts
        }
        
        // 生成加密的 cookie
        const cookieData = await encryptData(JSON.stringify({
            unique_name: user.name,
            timestamp: Date.now()
        }));
        
        const response = await displayAccountPage(user);
        response.headers.set('Set-Cookie', `user=${cookieData}; Path=/; HttpOnly; Secure; Max-Age=604800`); // 7天有效期
        return response;
    } catch (error) {
        return displayErrorPage(error.message);
    }
}

// 修改注销处理函数
async function handleLogout(request) {
    const formData = await request.formData();
    const unique_name = formData.get("unique_name");
    
    const response = new Response(displayTransitionPage("注销中", "正在安全退出您的账户...").body, {
        headers: { 
            "Content-Type": "text/html; charset=utf-8",
            "Set-Cookie": "user=; Path=/; HttpOnly; Secure; Max-Age=0"
        }
    });
    
    return response;
}

//获取组和组内账户
async function getGroupAccounts(groups, specificAccountKey = null) {
    // 验证 groups 参数
    if (!Array.isArray(groups) || groups.length === 0) {
        throw new Error("无效的用户组信息");
    }

    // 获取所有组的账户信息
    const groupPromises = groups.map(async groupName => {
        const groupJson = await KV.get(groupName);
        if (!groupJson) return {};
        
        try {
            const groupAccounts = JSON.parse(groupJson);
            
            // 如果指定了特定账户,检查该账户是否在允许的组中
            if (specificAccountKey) {
                const account = groupAccounts[specificAccountKey];
                if (account) {
                    return { [specificAccountKey]: { ...account, group: groupName } };
                }
                return {};
            }
            
            // 为每个账号添加组信息
            const accountsWithGroup = {};
            Object.entries(groupAccounts).forEach(([key, value]) => {
                accountsWithGroup[key] = { ...value, group: groupName };
            });
            
            return accountsWithGroup;
        } catch (error) {
            console.error(`解析组 ${groupName} 的数据时出错:`, error);
            return {};
        }
    });

    const results = await Promise.all(groupPromises);
    
    // 如果是查找特定账户,返回第一个匹配的账户
    if (specificAccountKey) {
        const foundAccount = results.find(result => result[specificAccountKey]);
        return foundAccount ? foundAccount[specificAccountKey] : null;
    }
    
    // 合并所有账户并返回
    return Object.assign({}, ...results);
}

// 处理账户选择
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 userJson = await KV.get(`${unique_name}`);
        if (!userJson) {
            return displayErrorPage("用户信息已失效,请重新登录");
        }
        
        const user = JSON.parse(userJson);
        
        // 获取账户信息
        const selectedAccount = user.account[account_key] || await getGroupAccounts(user.group, account_key);
        if (!selectedAccount) {
            return displayErrorPage("该账户不存在或您没有访问权限");
        }
        
        // 获取访问令牌
        let access_token;
        try {
            access_token = selectedAccount.access_token || await getValidToken(selectedAccount);
        } catch (error) {
            return displayErrorPage("获取访问令牌失败,请稍后重试");
        }
        
        // 生成共享令牌
        const shareToken = await generateShareToken(unique_name, access_token);
        if (!shareToken || shareToken === "未找到 Share_token") {
            return displayErrorPage("生成共享令牌失败,请稍后重试");
        }
        
        // 获取域名并生成跳转链接
        const YOUR_DOMAIN = (await KV.get("YOUR_DOMAIN")) || new URL(request.url).host;
        const oauthLink = await getOAuthLink(shareToken, YOUR_DOMAIN);
        if (!oauthLink) {
            return displayErrorPage("生成登录链接失败,请稍后重试");
        }
        
        return Response.redirect(oauthLink, 302);
    } catch (error) {
        console.error("账户选择处理错误:", error);
        return displayErrorPage(error.message || "处理请求时发生错误,请稍后重试");
    }
}

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

    const userJson = await KV.get(`${unique_name}`);
    if (!userJson) {
        throw new Error("用户不存在");
    }

    const user = JSON.parse(userJson);
    if (site_password !== user.password) {
        throw new Error("密码错误");
    }

    return user;
}

// 获取有效令牌
async function getValidToken(account) {
    if (!account.refresh_token) {
        throw new Error("访问令牌不存在,请联系管理员重新配置账户");
    }
    try {
        const token = await refreshToken(account.refresh_token);
        if (!token) {
            throw new Error("获取访问令牌失败");
        }
        account.access_token = token;
        return token;
    } catch (error) {
        if (error.message.includes("Error fetching access token")) {
            throw new Error("刷新访问令牌失败,请稍后重试");
        }
        throw error; // 抛出其他错误
    }
}

// 刷新 Token
async function refreshToken(refreshToken) {
    try {
        const url = "https://token.oaifree.com/api/auth/refresh";
        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) {
            const errorText = await response.text();
            throw new Error(`刷新令牌请求失败: ${response.status} - ${errorText}`);
        }
        
        const data = await response.json();
        if (!data.access_token) {
            throw new Error("返回的数据中没有访问令牌");
        }
        
        return data.access_token;
    } catch (error) {
        console.error("刷新令牌时出错:", error);
        throw new Error("刷新访问令牌失败,请检查网络连接或联系管理员");
    }
}

// 生成共享令牌
async function generateShareToken(unique_name, access_token) {
    const url = "https://chat.oaifree.com/token/register";

    const show_conversations = (await KV.get("show_conversations")) || "true";
    const temporary_chat = (await KV.get("temporary_chat")) || "false";
    const reset_limit = (await KV.get("reset_limit")) || "false";

    const body = new URLSearchParams({
        unique_name,
        access_token,
        site_limit: "",
        expires_in: "0",
        gpt35_limit: "-1",
        gpt4_limit: "-1",
        show_conversations,
        temporary_chat,
        reset_limit,
    });

    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;
}

// 显示登录页面
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>欢迎使用</title>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.0/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://img.pub/p/2539593fdcda772068e2.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" },
    });
}

// 显示账户选择页面
function displayAccountPage(user) {
    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/tailwindcss@2.0.0/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://img.pub/p/2539593fdcda772068e2.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; }
                .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="${user.name}">
                    <select name="account_key" class="select">
                        ${Object.keys(user.account).map(account => 
                            `<option value="${account}">${account}${user.account[account].group ? ` [${user.account[account].group}]` : ''}</option>`
                        ).join('')}
                    </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 mb-4">开始使用</button>
                </form>
                <form method="POST" action="/logout">
                    <input type="hidden" name="unique_name" value="${user.name}">
                    <button type="submit" class="w-full h-12 bg-red-500 hover:bg-red-600 text-white font-bold rounded-lg transition-colors duration-300">注销登录</button>
                </form>
            </div>
        </body>
        </html>
    `;
    return new Response(htmlContent, {
        headers: { "Content-Type": "text/html; charset=utf-8" },
    });
}

// 合并显示页面函数
function displayMessagePage(title, message, isError = false, autoRedirect = false) {
    const htmlContent = `
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <title>${title}</title>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.0/dist/tailwind.min.css" rel="stylesheet">
            <link rel="icon" type="image/png" href="https://img.pub/p/8efdf03e4c4a5f057ac6.jpg">
            ${autoRedirect ? '<meta http-equiv="refresh" content="1; url=/">' : ''}
            <style>
                body { background-image: url('https://img.pub/p/2539593fdcda772068e2.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: ${isError ? '#e53e3e' : '#4a5568'}; margin-bottom: 20px; }
                .button { display: inline-block; padding: 12px 24px; border-radius: 8px; font-weight: bold; transition: all 0.3s ease; text-decoration: none; width: 100%; }
                .primary-button { background-color: #48bb78; color: white; }
                .primary-button:hover { background-color: #38a169; }
                @keyframes spin {
                    to { transform: rotate(360deg); }
                }
                .loading-icon {
                    display: ${autoRedirect ? 'inline-block' : 'none'};
                    width: 40px;
                    height: 40px;
                    border: 4px solid #48bb78;
                    border-radius: 50%;
                    border-top-color: transparent;
                    animation: spin 1s linear infinite;
                    margin-bottom: 20px;
                }
            </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">${title}</h1>
                <div class="loading-icon"></div>
                <p class="message">${message}</p>
                ${!autoRedirect ? `
                    <a href="/" class="button primary-button">返回重试</a>
                ` : ''}
            </div>
        </body>
        </html>
    `;
    return new Response(htmlContent, {
        headers: { "Content-Type": "text/html; charset=utf-8" },
        status: isError ? 400 : 200
    });
}

// 更新错误页面显示函数
function displayErrorPage(errorMessage) {
    return displayMessagePage("发生错误", errorMessage, true, false);
}

// 更新过渡页面显示函数
function displayTransitionPage(title, message) {
    return displayMessagePage(title, message, false, true);
}

KV 配置

参考链接