功能介绍
此 Cloudflare Worker 可用于组建和分享Fuclaude 的session_key号池,依赖始皇的Fuclaude( 【v0.4.1】闲来无事搓的一个小玩具 )。
- 用户通过输入唯一名称 (Unique Name) 登录,每个用户的会话内容隔离(通过Fuclaude的 /manage-api/auth/oauth_token接口实现)。
- 用户登录成功后选择可用节点即可跳转。
站内其实已经有类似的worker了,估计都比我写的好,我刚学会,脚本是在Claude的帮助下完成的。但还是抛砖作为记录和分享,欢迎提bug(bug一定是Claude写的,和我没关系
)。
最终效果:
仿照 claude 官方的风格:
登录界面:
选车界面
喂饭教程
0. 登录 Cloudflare Dashboard。
主要会用到这三个功能:
1:创建 KV 命名空间
Workers → KV 命名空间。
创建 KV 命名空间 fuclaude_shared_keys:
有两个变量:
session_keys_list:用于存储所有session_key密钥(格式如["sk-ant-sid01-xxxx", "sk-ant-sid01-yyyy"])。user_list:用于存储用户列表(格式如["user1", "user2"])。方便分享给他人登录用。
session_key 的获取方法:
在 demo.fuclaude.com 登录claude账号后,打开https://demo.fuclaude.com/api/auth/session 即可。
2:创建 Worker
在 Workers & Pages 编个名称创建即可
如果没有自己的域名的话,这个worker链接也能用来访问(国内不稳定,可能得梯子):
3. 创建 Turnstile :
在Turnstile 新增站点:
可以把worker链接的域名和自己打算用的域名都添加进去:
创建后会获取到 站点密钥和密钥,下一步要用。
4:Worker的设置和代码
在变量 - KV 命名空间绑定,设置好KV名。
进入编辑 将后面 Worker js代码粘贴到编辑器中。
开头这部分代码,按照注释说明自行修改:
有域名的话在触发器添加自己的域名,比如 shareclaude.xxxxx.com:
备注
为了减少代码行数我把 worker.js代码中用到的css放到另外的位置了。
为了防止我跑路或服务故障,可以把这个css下载下来放到你信任的地方去。
worker.js代码:
// KV 命名空间,和【变量 - KV 命名空间绑定】中的名字保持一致
var KV = demo_shared_keys;
// fulaude的目标域名,自己搭建或者始皇的demo.fuclaude.com
const TARGET_DOMAIN = "demo.xxxxxxxx.com";
// 管理员账号,修改成不容易被猜到的一个名字,能看到用户列表,聊天记录不隔离,暂无其它功能。
const ADMIN_UNAME = "admin##xxxxx";
// 网站标题,自行修改
const SHARED_SITE_NAME = "Shared Demo";
// TURNSTILE的站点密钥
const TURNSTILE_SITE_KEY = "0x4AAAxxxxxxxxxxxx";
// TURNSTILE的密钥
const TURNSTILE_SECRET_KEY = "0x4AAAxxxxxxyyyyyyyxxxxxx";
// OAUTH_API 链接,如果是自己搭建的fuclaude,自行修改下。
const OAUTH_API_URL = "https://demo.fuclaude.com/manage-api/auth/oauth_token";
// 用新窗口打开,如果改成 false则为选号窗口直接打开
const OPEN_TARGET_IN_NEW = true;
// 登录页面的提示信息,文本或html
const SHOW_INFO_IN_LOGIN = `
<p style="margin:5px 0;">说明:</p>
<ul style="margin:0;">
<li> 使用unique Name登录后选择节点。</li>
<li> 每个用户(unique Name)之间的对话相互隔离。</li>
<li> 暂无注册方法,需要管理员添加账号。</li>
<li> Powered by <a style="color: inherit;" href="https://github.com/wozulong/fuclaude">fuclaude</a>。</li>
</ul>`;
// 选节点页面的提示信息,文本或html
const SHOW_INFO_IN_SELECT = `
<p style="margin:5px 0;">相关说明:</p>
<ul style="margin:0;">
<li> 状态idle/normal/busy仅供参考,该状态表示节点的近期点击频率,不能真实反映节点用量。</li>
<li> 如果节点出现warn状态,或者跳转后依然需要登录,可能是节点故障或失效,请尝试其它节点。</li>
<li> 每个用户(unique Name)之间的对话内容是独立的,每个节点之间对话内容也是独立的。</li>
<li> Claude限制:每轮对话最多约5万字,每个节点每天可用约50条对话,当提示Limited尝试更换节点。</li>
</ul>`;
/* main code */
const LOGIN_TOKEN_EXPIRY = 3600; // 1 hour
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
try {
const url = new URL(request.url);
if (request.method === 'GET') {
const uniqueName = url.searchParams.get('un');
const sessionToken = url.searchParams.get('token');
if (!uniqueName) {
return renderLoginPage();
} else if (sessionToken) {
// Verify the session token
if (await verifySessionToken(uniqueName, sessionToken)) {
return renderSelectAccountPage(uniqueName);
} else {
return Response.redirect(`${url.origin}`, 302);
}
} else {
return Response.redirect(`${url.origin}`, 302);
}
}
if (request.method === 'POST') {
if (request.url.includes('login')) {
return await handleLogin(request);
} else if (request.url.includes('selectKey')) {
return await handleSelectKey(request);
}
}
return new Response('Not Found', { status: 404 });
} catch (error) {
console.error('Error in handleRequest:', error);
return new Response(`Internal Server Error: ${error.message}`, {
status: 500,
headers: { 'Content-Type': 'text/plain' }
});
}
}
function renderLoginPage() {
return new Response(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="https://src.viewing.cc/img/claude.png">
<title>${SHARED_SITE_NAME} | login</title>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<link rel="stylesheet" type="text/css" href="https://src.viewing.cc/css/cssForDemo.css">
</head>
<body>
<form id="loginForm" action="/login" method="post">
<label for="uniqueName">Enter Unique Name:</label>
<input type="text" id="uniqueName" name="un" required pattern="[A-Za-z0-9_@#$]{3,}">
<div class="cf-turnstile" data-sitekey="${TURNSTILE_SITE_KEY}" data-callback="onTurnstileSuccess"></div>
<input type="hidden" id="cf-turnstile-response" name="cf-turnstile-response">
<input type="submit" value="Login" id="loginButton" disabled>
</form>
<div style="margin-top:1.5em;">${SHOW_INFO_IN_LOGIN}</div>
<script>
function onTurnstileSuccess(token) {
document.getElementById('cf-turnstile-response').value = token;
document.getElementById('loginButton').disabled = false;
}
document.getElementById('loginForm').addEventListener('submit', function(e) {
document.getElementById('loginButton').disabled = true;
document.body.style.cursor = "progress";
e.preventDefault();
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
un: document.getElementById('uniqueName').value,
'cf-turnstile-response': document.getElementById('cf-turnstile-response').value
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect;
} else {
alert(data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
})
.finally(() => {
setTimeout(function(){
document.getElementById('loginButton').disabled = false;
document.body.style.cursor = "";
}, 300);
});
});
</script>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
});
}
async function handleLogin(request) {
const { un, 'cf-turnstile-response': turnstileResponse } = await request.json();
if (!await verifyTurnstile(turnstileResponse)) {
return new Response(JSON.stringify({ success: false, message: 'Turnstile verification failed' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const uniqueName = un.trim().toLowerCase();
const usersJson = await KV.get('user_list');
let allUsers = JSON.parse(usersJson);
allUsers.push(ADMIN_UNAME);
if (!allUsers.includes(uniqueName)) {
return new Response(JSON.stringify({ success: false, message: 'Invalid uniqueName' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const sessionToken = await generateSessionToken(uniqueName);
const redirectUrl = `${new URL(request.url).origin}/?un=${encodeURIComponent(uniqueName)}&token=${encodeURIComponent(sessionToken)}`;
return new Response(JSON.stringify({ success: true, redirect: redirectUrl }), {
headers: { 'Content-Type': 'application/json' }
});
}
async function generateSessionToken(uniqueName) {
const token = crypto.randomUUID();
await KV.put(`session_${uniqueName}`, token, { expirationTtl: LOGIN_TOKEN_EXPIRY });
return token;
}
async function verifySessionToken(uniqueName, token) {
const storedToken = await KV.get(`session_${uniqueName}`);
return storedToken === token;
}
async function renderSelectAccountPage(uniqueName) {
const usersJson = await KV.get('user_list');
let allUsers = JSON.parse(usersJson);
allUsers.push(ADMIN_UNAME);
if (!allUsers.includes(uniqueName)) {
return new Response('Invalid uniqueName', { status: 400, headers: { 'Content-Type': 'text/plain' } });
}
const sessionKeysJson = await KV.get('session_keys_list');
const sessionKeys = JSON.parse(sessionKeysJson);
const oauthLogsJson = await KV.get('sk_get_oauth_logs');
const oauthLogs = oauthLogsJson ? JSON.parse(oauthLogsJson) : [];
const currentTime = new Date();
function countStatusOccurrences(data, nHours, status) {
const nHoursAgo = new Date(currentTime.getTime() - nHours * 60 * 60 * 1000);
return data.reduce((count, entry) => {
const entryTime = new Date(entry.timestamp);
const isValidStatus = status === null || entry.status === status;
if (isValidStatus && entryTime >= nHoursAgo) {
return count + 1;
}
return count;
}, 0);
}
let userInfoHtml;
if (ADMIN_UNAME === uniqueName) {
const reversedLogs = oauthLogs.slice(-20).reverse();
let showlogs = "";
reversedLogs.forEach(record => {
const sk_part = record.session_key.substr(0, 30);
showlogs += `| ${record.timestamp} | ${record.uname.padStart(12, ' ')} | ${record.status}; | ${sk_part}... | sk_${record.sk_index+1} |\n`;
});
userInfoHtml = `<div>当前是管理员</div>
<div>用户列表:<pre style="margin:0px 2em;">${usersJson.toString()}</pre></div>
<div>近期oauthLogs:<pre style="margin:0px 2em;">${showlogs}</pre></div>`;
} else {
userInfoHtml = `<div style="text-align:center;">当前用户:<span style="color:#696;">${uniqueName.trim()}</span></div>`;
}
let buttonHtml = '';
for (let i = 0; i < sessionKeys.length; i++) {
const key = sessionKeys[i];
const recentLogs = oauthLogs.filter(log => log.session_key === key).slice(-20);
let status = 'idle';
if (recentLogs.length > 0) {
const latestLog = recentLogs[recentLogs.length - 1];
if (latestLog.status === 'succ') {
status = 'normal';
let succCount = countStatusOccurrences(recentLogs, 12, 'succ');
if (succCount < 1) {
status = 'idle';
} else {
succCount = countStatusOccurrences(recentLogs, 8, 'succ');
if (succCount > 5)
status = 'busy';
else
status = 'normal';
}
} else {
const failCount = countStatusOccurrences(recentLogs, 8, 'fail');
if (failCount >= 2) {
status = 'warn';
}
}
}
buttonHtml += `<button class="${status}" onclick="selectKey(${i})">节点 ${i + 1} (${status})</button>`;
}
let open_link_html = "window.location.href = data.url;";
if(OPEN_TARGET_IN_NEW) {
open_link_html = "window.open(data.url, '_blank');";
}
return new Response(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="https://src.viewing.cc/img/claude.png">
<title>${SHARED_SITE_NAME} | Select Account</title>
<link rel="stylesheet" type="text/css" href="https://src.viewing.cc/css/cssForDemo.css">
</head>
<body>
<div class="main">
<div class="container">
<h2>Select Account</h2>
<div>${buttonHtml}</div>
</div>
<div style="color:#888; font-size:medium; margin:15px">
${userInfoHtml}
<div style="margin-top:1.5em;">${SHOW_INFO_IN_SELECT}</div>
</div>
</div>
<script>
function selectKey(index) {
document.querySelectorAll("button").forEach(button => button.disabled = true);
document.body.style.cursor = "progress";
fetch('/selectKey', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ index, uniqueName: "${uniqueName}" })
}).then(response => response.json()).then(data => {
if (data.url) {
${open_link_html}
} else {
alert(data.message);
}
}).catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
}).finally(() => {
setTimeout(function(){
document.querySelectorAll("button").forEach(button => button.disabled = false);
document.body.style.cursor = "";
}, 1000);
});
}
</script>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
});
}
async function verifyTurnstile(turnstileResponse) {
const turnstileVerifyResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `secret=${TURNSTILE_SECRET_KEY}&response=${turnstileResponse}`
});
const turnstileVerifyData = await turnstileVerifyResponse.json();
return turnstileVerifyData.success;
}
async function handleUniqueNameLogin(uniqueName, requestUrl) {
uniqueName = uniqueName.trim().toLowerCase();
const usersJson = await KV.get('user_list');
let allUsers = JSON.parse(usersJson);
allUsers.push(ADMIN_UNAME);
if (!allUsers.includes(uniqueName)) {
return new Response('Invalid uniqueName', { status: 400, headers: { 'Content-Type': 'text/plain' } });
}
return Response.redirect(`${new URL(requestUrl).origin}/?un=${encodeURIComponent(uniqueName)}`, 302);
}
async function handleSelectKey(request) {
const body = await request.json();
const index = body.index;
const uniqueName = body.uniqueName;
const sessionKeysJson = await KV.get('session_keys_list');
const sessionKeys = JSON.parse(sessionKeysJson);
const usersJson = await KV.get('user_list');
let allUsers = JSON.parse(usersJson);
allUsers.push(ADMIN_UNAME);
if (!uniqueName || !allUsers.includes(uniqueName)) {
return new Response(JSON.stringify({ message: 'Invalid unique name' }), {
headers: { 'Content-Type': 'application/json' },
status: 400
});
}
if (!sessionKeys || !sessionKeys[index]) {
return new Response(JSON.stringify({ message: 'Invalid or expired session key' }), {
headers: { 'Content-Type': 'application/json' },
status: 400
});
}
const sessionKey = sessionKeys[index];
const redirectUrl = await getOauthTokenURL(sessionKey, uniqueName, index);
const logEntry = {
timestamp: new Date().toISOString(),
session_key: sessionKey,
sk_index: index,
uname: uniqueName,
status: redirectUrl ? 'succ' : 'fail'
};
const oauthLogsJson = await KV.get('sk_get_oauth_logs');
let oauthLogs = oauthLogsJson ? JSON.parse(oauthLogsJson) : [];
oauthLogs.push(logEntry);
await KV.put('sk_get_oauth_logs', JSON.stringify(oauthLogs));
if (redirectUrl) {
return new Response(JSON.stringify({ url: redirectUrl }), {
headers: { 'Content-Type': 'application/json' }
});
} else {
return new Response(JSON.stringify({ message: `Get token fail, ret=${redirectUrl}, uname=${uniqueName}` }), {
headers: { 'Content-Type': 'application/json' },
status: 400
});
}
}
async function getOauthTokenURL(key, uname, index) {
if (typeof key !== 'string' || !key.startsWith('sk-ant-sid01-') || key.length <= 20) {
console.error('key format err');
return -1;
}
if (uname == ADMIN_UNAME) {
uname = null;
}
try {
let response = await fetch(`${OAUTH_API_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
session_key: key,
unique_name: uname,
expires_in: 86400
})
});
let data = await response.json();
return `https://${TARGET_DOMAIN}${data.login_url}`;
} catch (error) {
console.error('Error getting OAuth token URL:', error);
return false;
}
}













