【Fuclaude 号池】docker部署

接着号池Node版本继续讨论,没有docker版本怎么能行呢,让Claude略微修改下页面,打了个镜像:

镜像地址:

linqiu1199/claude-pool:latest

环境变量说明:

ORIGINAL_WEBSITE=镜像站地址,不填默认为https://demo.fuclaude.com⁠

SESSION_KEYS=sessionKey,多个以逗号分隔(必填)

SITE_PASSWORD=站点密码(必填)

GUEST_PASSWORD=访客密码(必填)

预览图:

如果你使用自己的镜像站,那可以设置点击退出登录时跳转到号池首页,在镜像站的nginx下配置登出时重定向即可:

    location = /logout {
        return 301 你的号池地址/;
    }

源代码:

app.js(修复sessionkey暴露问题):

const express = require('express');
const fetch = require('node-fetch');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;

const CONFIG = {
  ORIGINAL_WEBSITE: process.env.ORIGINAL_WEBSITE || "https://demo.fuclaude.com",
  SESSION_KEYS: process.env.SESSION_KEYS ? process.env.SESSION_KEYS.split(',') : [],
  SITE_PASSWORD: process.env.SITE_PASSWORD || "m",
  GUEST_PASSWORD: process.env.GUEST_PASSWORD || "m"
};

app.use(bodyParser.urlencoded({ extended: true }));

app.get('/', (req, res) => {
  res.send(formHtml);
});

app.post('/login', async (req, res) => {
  try {
    const loginType = req.body.login_type;
    const selectedKeyIndex = req.body.session_key_index;

    let body = {
      'session_key': CONFIG.SESSION_KEYS[selectedKeyIndex]
    };

    if (loginType === 'site') {
      const sitePassword = req.body.site_password;
      if (sitePassword !== CONFIG.SITE_PASSWORD) {
        return res.status(403).send('Incorrect site password');
      }
    } else if (loginType === 'guest') {
      const username = req.body.username;
      const guestPassword = req.body.guest_password;
      
      if (!username || username.trim() === '') {
        return res.status(400).send('Username is required for guest login');
      }
      
      if (guestPassword !== CONFIG.GUEST_PASSWORD) {
        return res.status(403).send('Incorrect guest password');
      }

      body.unique_name = username;
    } else {
      return res.status(400).send('Invalid login type');
    }

    const authUrl = `${CONFIG.ORIGINAL_WEBSITE}/manage-api/auth/oauth_token`;

    const apiResponse = await fetch(authUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    });

    if (!apiResponse.ok) {
      throw new Error(`API request failed with status ${apiResponse.status}`);
    }

    const respJson = await apiResponse.json();
    const login_url = respJson.login_url || '/';
    res.redirect(`${CONFIG.ORIGINAL_WEBSITE}${login_url}`);
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).send('An error occurred during login');
  }
});

app.use(async (req, res) => {
  try {
    const newUrl = `${CONFIG.ORIGINAL_WEBSITE}${req.url}`;
    const response = await fetch(newUrl, {
      method: req.method,
      headers: req.headers,
      body: req.method !== 'GET' && req.method !== 'HEAD' ? req.body : undefined,
      redirect: 'follow'
    });

    const contentType = response.headers.get('content-type');
    if (contentType && contentType.includes('text/html') && !req.url.includes('/login_oauth')) {
      let html = await response.text();
      const regex = /<div[^>]*>(?=[\s\S]*?<h3[\s\S]*?<\/h3>)(?=[\s\S]*?<p[\s\S]*?<\/p>)(?=[\s\S]*?<div[\s\S]*?<\/div>)[\s\S]*?<\/div>/gi;
      html = html.replace(regex, '');
      res.send(html);
    } else {
      res.status(response.status).set(response.headers).send(await response.buffer());
    }
  } catch (error) {
    console.error('Proxy error:', error);
    res.status(500).send('An error occurred during the proxy request');
  }
});

const formHtml = `
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Claude 账号池</title>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r121/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vanta.net.min.js"></script>
    <style>
        body, html {
            height: 100%;
            margin: 0;
            font-family: 'Noto Sans SC', sans-serif;
            background-color: transparent;
            position: relative;
        }
        #vanta-background {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 1;
        }
        .container {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100%;
            padding: 20px;
            position: relative;
            z-index: 2;
        }
        .form-container {
            background: rgba(255, 255, 255, 0.8);
            padding: 40px;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            width: 100%;
            max-width: 400px;
            transition: all 0.3s ease;
            backdrop-filter: blur(10px);
        }
        h1 {
            color: #4a5568;
            text-align: center;
            margin-bottom: 30px;
            font-size: 28px;
            font-weight: 500;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 8px;
            color: #4a5568;
            font-weight: 500;
        }
        input[type="text"], input[type="password"] {
            width: 100%;
            padding: 12px;
            border: 2px solid #e2e8f0;
            border-radius: 8px;
            font-size: 16px;
            transition: all 0.3s ease;
        }
        input[type="text"]:focus, input[type="password"]:focus {
            outline: none;
            border-color: #6e8efb;
            box-shadow: 0 0 0 3px rgba(110, 142, 251, 0.1);
        }
        button {
            width: 100%;
            padding: 12px;
            background-color: #6e8efb;
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.3s ease;
            margin-bottom: 10px;
        }
        button:hover {
            background-color: #5a67d8;
            transform: translateY(-2px);
            box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
        }
        .key-buttons {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
            gap: 15px;
            margin-bottom: 20px;
        }
        .key-button {
            padding: 15px;
            background-color: #4a5568;
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s ease;
            text-align: center;
            font-weight: 500;
        }
        .key-button:hover {
            background-color: #2d3748;
            transform: translateY(-2px);
            box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
        }
        .key-button.selected {
            background-color: #6e8efb;
            transform: scale(1.05);
        }
        .hidden {
            display: none;
        }
        .switch-btn {
            background-color: transparent;
            color: #6e8efb;
            border: 2px solid #6e8efb;
        }
        .switch-btn:hover {
            background-color: rgba(110, 142, 251, 0.1);
        }
    </style>
</head>
<body>
    <div id="vanta-background"></div>
    <div class="container">
        <div class="form-container" id="keySelection">
            <h1>Claude 账号池</h1>
            <div class="key-buttons" id="keyButtons">
                <!-- 账号按钮将在这里动态生成 -->
            </div>
        </div>

        <div class="form-container hidden" id="guestLogin">
            <h1>访客登录</h1>
            <form method="POST" action="/login">
                <input type="hidden" name="login_type" value="guest">
                <input type="hidden" id="guest_selected_key_index" name="session_key_index" value="">
                <div class="form-group">
                    <label for="guest_password">访客密码:</label>
                    <input type="password" id="guest_password" name="guest_password" placeholder="请输入访客密码" required>
                </div>
                <div class="form-group">
                    <label for="username">用户名:</label>
                    <input type="text" id="username" name="username" placeholder="请输入一个唯一的用户名" required>
                </div>
                <button type="submit">登录</button>
            </form>
            <button class="switch-btn" onclick="toggleForm()">切换到站点密码登录</button>
            <button class="switch-btn" onclick="backToKeySelection()">返回账号选择</button>
        </div>

        <div class="form-container hidden" id="siteLogin">
            <h1>站点密码登录</h1>
            <form method="POST" action="/login">
                <input type="hidden" name="login_type" value="site">
                <input type="hidden" id="site_selected_key_index" name="session_key_index" value="">
                <div class="form-group">
                    <label for="site_password">站点密码:</label>
                    <input type="password" id="site_password" name="site_password" placeholder="请输入站点密码" required>
                </div>
                <button type="submit">登录</button>
            </form>
            <button class="switch-btn" onclick="toggleForm()">切换到访客登录</button>
            <button class="switch-btn" onclick="backToKeySelection()">返回账号选择</button>
        </div>
    </div>

    <script>
        const keyCount = ${CONFIG.SESSION_KEYS.length};
        
	    function createKeyButtons() {
	        const container = document.getElementById('keyButtons');
	        for (let i = 0; i < keyCount; i++) {
	            const button = document.createElement('button');
	            button.type = 'button';
	            button.className = 'key-button';
	       	  button.textContent = '账号 ' + (i + 1);
	            button.onclick = () => selectKey(i);
	            container.appendChild(button);
	        }
	    }

        function selectKey(index) {
            document.getElementById('guest_selected_key_index').value = index;
            document.getElementById('site_selected_key_index').value = index;
            document.getElementById('keySelection').classList.add('hidden');
            document.getElementById('guestLogin').classList.remove('hidden');
        }

        function backToKeySelection() {
            document.getElementById('keySelection').classList.remove('hidden');
            document.getElementById('guestLogin').classList.add('hidden');
            document.getElementById('siteLogin').classList.add('hidden');
        }

        function toggleForm() {
            var guestLogin = document.getElementById('guestLogin');
            var siteLogin = document.getElementById('siteLogin');
            if (guestLogin.classList.contains('hidden')) {
                guestLogin.classList.remove('hidden');
                siteLogin.classList.add('hidden');
            } else {
                guestLogin.classList.add('hidden');
                siteLogin.classList.remove('hidden');
            }
        }

        // 初始化按钮
        createKeyButtons();

        // 初始化 Vanta.js 背景
        VANTA.NET({
            el: "#vanta-background",
            mouseControls: true,
            touchControls: true,
            gyroControls: false,
            minHeight: 200.00,
            minWidth: 200.00,
            scale: 1.00,
            scaleMobile: 1.00,
            color: 0x3f51b5,
            backgroundColor: 0xffffff,
            points: 10.00,
            maxDistance: 25.00,
            spacing: 17.00
        });
    </script>
</body>
</html>
`;

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
  console.log(`Using original website: ${CONFIG.ORIGINAL_WEBSITE}`);
});

package.json文件:

{
  "name": "claude-account-pool",
  "version": "1.0.0",
  "description": "A proxy server for Claude account pool",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "node-fetch": "^2.6.1",
    "body-parser": "^1.19.0",
    "three": "^0.132.2",
    "vanta": "^0.5.21"
  }
}
47 个赞

给大佬点赞

:tieba_048:点赞

正好用得上

这部分用我最新的,会泄漏key到前端

OK,明天更新一下。

大佬:cow::beer:

佬你真强

太强了!

docker 具体配置文件有么

运行命令:

docker run -d \
  --name claude-pool \
  -p 3000:3000 \
  --restart always \
  -e ORIGINAL_WEBSITE=镜像站地址 \
  -e SESSION_KEYS=sk1,sk2,sk3 \
  -e SITE_PASSWORD=站点密码 \
  -e GUEST_PASSWORD=访客密码 \
  linqiu1199/claude-pool:latest

环境变量上面已经标明了。

2 个赞

端口从日志文件里看到了,已经用上了,感谢佬,能不能访客访问提前限定用户ID呢,避免被滥用啊

可以,有空加一下。

1 个赞

这个好

不错

牛的

感谢佬

感谢分享

您好 我在云服务上部署了 然后选择访客登陆后 报错了 An error occurred during login

先MARK,有空研究下