接着号池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"
}
}


