【Fuclaude号池worker代码,含教程】在Claude的帮助下糊个Claude风格的Fuclaude号池分享的cf-worker

功能介绍

此 Cloudflare Worker 可用于组建和分享Fuclaude 的session_key号池,依赖始皇的Fuclaude( 【v0.4.1】闲来无事搓的一个小玩具 )。

  • 用户通过输入唯一名称 (Unique Name) 登录,每个用户的会话内容隔离(通过Fuclaude的 /manage-api/auth/oauth_token接口实现)。
  • 用户登录成功后选择可用节点即可跳转。

站内其实已经有类似的worker了,估计都比我写的好,我刚学会,脚本是在Claude的帮助下完成的。但还是抛砖作为记录和分享,欢迎提bug(bug一定是Claude写的,和我没关系 :tieba_025:)。

最终效果:

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

186 个赞

帮顶

6 个赞

2 个赞

水龙王住在L站吗 :tieba_027:

4 个赞

不错不错,马上试试看

4 个赞

谢顶

1 个赞

感谢分享.

2 个赞

mark

1 个赞

mark

1 个赞

点赞

3 个赞

厉害啊大佬!

1 个赞

mark

1 个赞

这个不错,感谢大佬分享

2 个赞

mark

1 个赞

看着不错

1 个赞

感谢你的消息

1 个赞

6666

强,支持下

1 个赞

需要部署始皇佬的fuclaude的后端嘛

1 个赞

mark