新fuclaude的账户切换和对话隔离方案,解决跨域请求问题(方案就是不跨域)

最新的demofuclaude修改了跨域请求的策略导致一系列问题,现在我糊了一个简单的解决方案,
支持对话隔离,支持多账户切换,方便几个朋友间拼车使用,强烈谴责商人滥用始皇公共资源
基于

做法就是通过前台脚本来直接操作demo.fuclaude.com,这样就不用跨域请求了(天才的想法)
这样就做了一个cloudflare的worker后端和油猴或者脚本猫脚本的前端
worker的部署和key的设置参考了上面的佬友办法,其中your-encryption-key 是一个简单的加密(我也不知道安不安全,假装安全了)设置前后端一致即可,SITE_PASSWORD是站点密码,这是后端的,应该存储在kv里的。
下面是浏览器的脚本文件,好像触发了什么关键词,所以只能上传文件了,并且后缀由js改成了txt,放进油猴或者脚本猫
tampermonkey_script_opensource.txt (23.9 KB)

下面是worker代码,粘贴进cloudflare然后参照上面佬友的那个kv设置kv

var KV = kv_shared_storage; 
const HOST = 'demo.example.com'; 
const BASE_URL = `https://${HOST}`;
const AUTH_ENDPOINT = '/manage-api/auth/oauth_token';
const ENCRYPTION_KEY = 'your-encryption-key'; 

// Simple encryption function (matches frontend)
function simpleEncrypt(text, key) {
  try {
    let encrypted = '';
    for (let i = 0; i < text.length; i++) {
      const keyChar = key.charCodeAt(i % key.length);
      const textChar = text.charCodeAt(i);
      const encryptedChar = textChar ^ keyChar;
      encrypted += encryptedChar.toString(16).padStart(2, '0');
    }
    return encrypted;
  } catch (e) {
    console.error('Encryption failed:', e);
    return text; // Return original text if encryption fails
  }
}

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

// Filter accounts function - customize as needed
function filterAccounts(accounts) {
  const filtered = {};
  for (const [key, value] of Object.entries(accounts)) {
    // Add your filtering logic here
    // Example: if (key !== 'restricted_account') { filtered[key] = value; }
    filtered[key] = value;
  }
  return filtered;
}

async function generateLoginHtml(accounts, showAllAccounts = false) {
  // Filter accounts based on permissions
  const filteredAccounts = showAllAccounts ? accounts : filterAccounts(accounts);
  
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Account Switcher</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        background-color: #f4f4f4;
        padding: 20px;
      }
      form, .response-container {
        background: white;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        max-width: 600px;
        margin: 20px auto;
      }
      h1 {
        text-align: center; 
      }
      h2 {
        text-align: center; 
      }
      p {
        display: block;
        margin-bottom: 10px;
        font-size: 16px;
      }
      input[type="text"], textarea {
        width: calc(100% - 22px);
        padding: 10px;
        margin-top: 5px;
        margin-bottom: 20px;
        border-radius: 5px;
        border: 1px solid #ccc;
      }
      textarea {
        font-family: 'Courier New', monospace;
        background-color: #f8f8f8;
        height: 150px;
      }
      button {
        background-color: #000000;
        color: white;
        padding: 10px 20px;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        font-size: 16px;
        font-weight:600;
        width:100% !important
      }
      button:hover {
        background-color: #1e293b;
      }
      @media (max-width: 768px) {
        body, form, .response-container {
          padding: 10px;
        }
      }
      .checkbox-group {
        display: flex;
        justify-content: space-between;
      }
      .checkbox-group input[type="checkbox"] {
        margin-right: 5px;
      }
      .checkbox-group label {
        margin-right: 10px;
      }
      select {
        width: calc(100% - 22px);
        padding: 10px;
        margin-top: 5px;
        margin-bottom: 20px;
        border-radius: 5px;
        border: 1px solid #ccc;
        font-size: 16px;
        background-color: white;
      }
      select:focus {
        outline: none;
        border-color: #000;
      }
    </style>
</head>

<body>
<h1>Account Switcher</h1>
<form method="POST">
  <label for="account-select">Please select an account:</label>
  <select id="account-select" name="account">
    <option value="" disabled selected>Please select an account</option>
    ${Object.keys(filteredAccounts).map((nickname) => `
      <option value="${nickname}">${nickname}</option>
    `).join('')}
  </select>
  <br/>

  <label for="site_password">Please enter site password:</label>
  <input type="text" id="site_password" name="site_password" placeholder="Site password">

  <button type="submit">Access</button>
</form>

<script>
document.querySelector('form').addEventListener('submit', async function(e) {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    const account = formData.get('account');
    const sitePassword = formData.get('site_password');
    
    if (!account) {
        alert('Please select an account');
        return;
    }
    
    try {
        const response = await fetch(window.location.href, {
            method: 'POST',
            body: formData
        });
        
        if (response.ok) {
            const result = await response.json();
            if (result.session_key) {
                alert('Verification successful! Please install the Tampermonkey script for automatic account switching.\\n\\nThe script will show an account switching panel on the target domain.');
            } else {
                alert('Failed to get session_key');
            }
        } else {
            const errorText = await response.text();
            alert('Error: ' + errorText);
        }
    } catch (error) {
        alert('Request failed: ' + error.message);
    }
});
</script>

</body>
</html>`;
}

async function handleRequest(request) {
  const requestURL = new URL(request.url);

  const accountsJsonStr = await KV.get('account_nickname_to_session_keys');
  const accounts = JSON.parse(accountsJsonStr);
  
  // Return accounts list
  if (request.method === "GET") {
    // Check password in URL parameters
    const urlPassword = requestURL.searchParams.get('password');
    const ADMIN_PASSWORD = 'admin-password'; // Replace with your admin password
    const showAllAccounts = urlPassword === ADMIN_PASSWORD;
    
    const html = await generateLoginHtml(accounts, showAllAccounts);
    return new Response(html, {
      headers: {
        'Content-Type': 'text/html; charset=utf-8'
      },
    });
  }

  // Handle account login
  if (request.method === "POST") {
    try {
      const formData = await request.formData();
      const account = formData.get("account");
      const sitePassword = formData.get('site_password') || '';

      // Admin password check
      const ADMIN_PASSWORD = 'admin-password'; // Replace with your admin password
      const showAllAccounts = sitePassword === ADMIN_PASSWORD;
      
      // KV storage password check
      const SITE_PASSWORD = await KV.get('SITE_PASSWORD') || '';
      if (sitePassword !== SITE_PASSWORD && sitePassword !== ADMIN_PASSWORD) {
        return new Response('Incorrect password', { status: 401 });
      }

      // Check account permissions (customize as needed)
      if (!showAllAccounts) {
        // Add your permission logic here
        // Example: if (account === 'restricted_account') { return new Response('No permission', { status: 403 }); }
      }

      const sessionKey = accounts[account];
      if (!sessionKey) {
        return new Response('Account not found', { status: 400 });
      }
      
      // Encrypt session_key before returning
      const encryptedSessionKey = simpleEncrypt(sessionKey, ENCRYPTION_KEY);
      return new Response(JSON.stringify({
        session_key: encryptedSessionKey,
        message: 'success'
      }), {
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': `https://${HOST}`,
          'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type'
        },
      });
      
    } catch (error) {
      return new Response(`Processing error: ${error.message}`, { status: 500 });
    }
  }
}
10 个赞

感谢大佬。

感谢分享

1 个赞

热佬好,油猴脚本呢 :joy:


这个文件就是油猴的

感谢佬~ :tieba_067:

感谢佬友接力。

这个方案使用起来很方便,但不足是需要用户主动下载插件以及sessionKey会被用户用DevTool抓包拿到。

实际上demo.fuclaude返回的login_url是不会过期的,所以我目前是直接采集车友们常见的nickname,然后手动在DevTool中的console发请求拿到login_url缓存在KV里,具体来说分为以下几步:

  1. 车友使用nickname登录。
  2. worker从KV找到该nickname&sessionKey有没有对应的login_url。
  3. 如有,直接返回login_url,让用户跳转demo站。
  4. 如无,记录成login_url=“unknown”。我偶尔上去或车友提醒我上去看看,批量在DevTool的Console向demo站发请求获取login_url,然后写回KV使得下次该车友登录时走以上第1,2,3步。

从oaifree到fuclaude,经历了ChatGPT降智、Claude封号,也不过各1年多。
终于觉得能跑起来的解决需求的方案就是好方案,至于说完美的优雅方案,万一过几天形势又变了呢。

1 个赞

所以也没真开发什么加密,就一个简单的算法前后端对照了就可以出来,就是防一下扫cf的worker给我扫出来。另外跳转页面也是跨域了,所以实际上只能自行复制访问。所以最快捷的还是安装一次脚本,以后直接切换。

1 个赞

佬,你这是3天前就搞出来了? 前两天一直在找


显示有歧义,其实这就是完整的两个文件

搞的太麻烦了,我最后自己搭建然后用cloudflare反代了,还是很舒服的

佬有考虑写个教程贴吗

谢谢佬友的分享