1. 搭建fuclaude
注意要在境外服务器上构建!
使用docker构建Fuclaude
docker run -d \
--name Fuclaude \
--restart=always \
-p 14300:8181 \
docker.hlyun.org/pengzhile/fuclaude:latest
在1panel面板中设置反代以实现域名访问。
2. 构建CF workers以实现访问隔离
ORIGINAL_WEBSITE: "输入第一步构建的反代域名",
SESSION_KEYS: [""], // 在此处加入你的 session_key
SITE_PASSWORD: "站点密码",
GUEST_PASSWORD: "访客密码"
const CONFIG = {
ORIGINAL_WEBSITE: "",
SESSION_KEYS: [""],
SITE_PASSWORD: "",
GUEST_PASSWORD: ""
};
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
if (url.pathname === '/login') {
return handleRootPath(request, url, true);
}
if (url.pathname === '/') {
return handleRootPath(request, url);
}
return proxyRequest(request);
}
async function handleRootPath(request, url, forceLogin = false) {
const cookie = request.headers.get('Cookie') || '';
if ((!forceLogin) && cookie.includes('_Secure-next-auth.session-data')) {
return Response.redirect(`${url.origin}/new`, 302);
}
if (request.method === 'POST') {
return handleLogin(request, url);
}
return new Response(formHtml, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
// 修改handleLogin函数
async function handleLogin(request, url) {
try {
const formData = await request.formData();
const loginType = formData.get('login_type');
let body = {
'session_key': getRandomSessionKey()
};
if (loginType === 'site') {
const sitePassword = formData.get('site_password');
if (sitePassword !== CONFIG.SITE_PASSWORD) {
return new Response('站点密码错误', {
status: 403,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
} else if (loginType === 'guest') {
const username = formData.get('username');
const guestPassword = formData.get('guest_password');
if (!username || username.trim() === '') {
return new Response('访客登录必须提供用户名', {
status: 400,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
// 检查用户名是否在白名单中
const isWhitelisted = await fuclaude.get(`user:${username.trim()}`);
if (!isWhitelisted) {
return new Response('该用户名未在白名单中', {
status: 403,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
if (guestPassword !== CONFIG.GUEST_PASSWORD) {
return new Response('访客密码错误', {
status: 403,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
body.unique_name = username;
} else {
return new Response('无效的登录类型', {
status: 400,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
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 || '/';
return Response.redirect(`https://${url.host}${login_url}`, 302);
} catch (error) {
console.error('Login error:', error);
return new Response('登录过程中发生错误', {
status: 500,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
}
async function proxyRequest(request) {
const url = new URL(request.url);
const newUrl = `${CONFIG.ORIGINAL_WEBSITE}${url.pathname}${url.search}`;
const modifiedRequest = new Request(newUrl, request);
const response = await fetch(modifiedRequest);
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('text/html') && url.pathname !== '/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, '');
return new Response(html, {
headers: response.headers
})
}
return response;
}
function getRandomSessionKey() {
const keys = CONFIG.SESSION_KEYS;
const randomIndex = Math.floor(Math.random() * keys.length);
return keys[randomIndex];
}
const formHtml = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fuclaude 登录</title>
<style>
body, html {
height: 100%;
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #F6F5F1; /* Claude's warm beige background */
color: #484544; /* Claude's dark text color */
}
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 20px;
}
.form-container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
width: 100%;
max-width: 400px;
border: 1px solid #E5E3DD; /* Claude's subtle border color */
}
h1 {
color: #484544;
text-align: center;
margin-bottom: 30px;
font-size: 24px;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #6C6867; /* Claude's secondary text color */
font-weight: 500;
font-size: 14px;
}
input[type="text"] {
width: 100%;
padding: 12px;
border: 1px solid #E5E3DD;
border-radius: 8px;
font-size: 16px;
transition: all 0.2s ease;
background-color: #FFFFFF;
color: #484544;
box-sizing: border-box;
}
input[type="text"]:focus {
outline: none;
border-color: #C24B00; /* Claude's orange accent color */
box-shadow: 0 0 0 3px rgba(194, 75, 0, 0.1);
}
button {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 10px;
}
button[type="submit"] {
background-color: #C24B00; /* Claude's orange button color */
color: white;
}
button[type="submit"]:hover {
background-color: #A33F00; /* Darker orange for hover */
}
.switch-btn {
background-color: #F6F5F1; /* Claude's background color */
color: #6C6867;
margin-top: 20px;
border: 1px solid #E5E3DD;
}
.switch-btn:hover {
background-color: #ECEAE6; /* Slightly darker beige for hover */
}
.hidden {
display: none;
}
input[type="text"]::placeholder {
color: #A8A6A4; /* Claude's placeholder text color */
}
@media (max-width: 480px) {
.form-container {
padding: 30px 20px;
}
}
.form-container {
transition: opacity 0.3s ease;
}
.form-container.hidden {
opacity: 0;
}
</style>
</head>
<body>
<div class="container">
<div class="form-container" id="guestLogin">
<h1>访客登录</h1>
<form method="POST">
<input type="hidden" name="login_type" value="guest">
<div class="form-group">
<label for="guest_password">访客密码</label>
<input type="text" 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>
</div>
<div class="form-container hidden" id="siteLogin">
<h1>站点密码登录</h1>
<form method="POST">
<input type="hidden" name="login_type" value="site">
<div class="form-group">
<label for="site_password">站点密码</label>
<input type="text" id="site_password" name="site_password" placeholder="请输入站点密码" required>
</div>
<button type="submit">登录</button>
</form>
<button class="switch-btn" onclick="toggleForm()">切换到访客登录</button>
</div>
</div>
<script>
function toggleForm() {
const guestLogin = document.getElementById('guestLogin');
const siteLogin = document.getElementById('siteLogin');
if (guestLogin.classList.contains('hidden')) {
siteLogin.style.opacity = '0';
setTimeout(() => {
guestLogin.classList.remove('hidden');
siteLogin.classList.add('hidden');
setTimeout(() => {
guestLogin.style.opacity = '1';
}, 50);
}, 300);
document.title = "Fuclaude 登录";
} else {
guestLogin.style.opacity = '0';
setTimeout(() => {
guestLogin.classList.add('hidden');
siteLogin.classList.remove('hidden');
setTimeout(() => {
siteLogin.style.opacity = '1';
}, 50);
}, 300);
document.title = "Fuclaude 站点密码登录";
}
}
</script>
</body>
</html>
`;
3. 绑定KV以开启白名单
在KV中新建命名空间并写入
在workers-设置-绑定中绑定所建的命名空间,部署即可。
注意绑定命名空间时的名称需和代码中isWhitelisted函数中的一致(默认可设置为fuclaude)。