- // ==UserScript==
- // @name Claude SessionKey Manager
- // @version 3.1
- // @description 轻松管理和切换Claude的SessionKey(可拖拽浮动按钮,简洁弹出式菜单)。此脚本基于 https://greasyfork.org/scripts/501296,原作者: https://greasyfork.org/zh-CN/users/1337296
- // @match https://claude.ai/*
- // @match https://demo.fuclaude.com/*
- // @grant none
- // @license GNU GPLv3
- // @author f14xuanlv
- // @namespace https://greasyfork.org/users/1454591
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // =============== 配置项 ===============
- const tokens = [
- {name: 'default_empty', key: 'sk-ant-sid01-xxx'},
- {name: 'default_empty1', key: 'sk-ant-sid01-xxx'},
- // 此处添加更多的SessionKey
- ];
-
- // =============== 样式设置 ===============
- const styles = document.createElement('style');
- styles.textContent = `
- .skm-main-button {
- position: fixed;
- z-index: 10000;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: #C96442;
- color: white;
- font-size: 16px;
- font-weight: bold;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- cursor: pointer;
- user-select: none;
- touch-action: none;
- border: none;
- }
-
- .skm-main-button:hover {
- background: #E67816;
- }
-
- .skm-dragging {
- opacity: 0.8;
- }
-
- .skm-popup {
- position: fixed;
- z-index: 9999;
- width: 280px;
- background-color: #FFF8F0 !important;
- border-radius: 8px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
- overflow: hidden;
- transition: opacity 0.2s ease;
- opacity: 0;
- pointer-events: none;
- }
-
- .skm-popup.active {
- opacity: 1;
- pointer-events: all;
- }
-
- .skm-popup-header {
- background: #C96442;
- color: white !important;
- padding: 12px 16px;
- font-weight: bold;
- font-size: 15px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .skm-close-btn {
- background: none;
- border: none;
- color: white !important;
- font-size: 18px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- border-radius: 50%;
- }
-
- .skm-close-btn:hover {
- background-color: rgba(255, 255, 255, 0.2);
- }
-
- .skm-popup-content {
- padding: 16px;
- background-color: #FFF8F0 !important;
- }
-
- .skm-token-list {
- margin-bottom: 16px;
- max-height: 200px;
- overflow-y: auto;
- }
-
- .skm-token-item {
- padding: 10px;
- margin-bottom: 8px;
- border-radius: 6px;
- border: 1px solid #E8DFD5;
- background-color: white !important;
- display: flex;
- justify-content: space-between;
- align-items: center;
- cursor: pointer;
- transition: all 0.2s;
- color: #333 !important;
- }
-
- .skm-token-item:hover {
- background-color: #FFF5EA !important;
- border-color: #C96442;
- color: #333 !important;
- }
-
- .skm-token-item.active {
- background-color: #FFF0DD !important;
- border-color: #C96442;
- color: #333 !important;
- }
-
- .skm-token-name {
- flex-grow: 1;
- font-size: 14px;
- padding-right: 8px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #333 !important;
- }
-
- .skm-use-btn {
- background-color: #C96442;
- color: white !important;
- border: none;
- border-radius: 4px;
- padding: 4px 8px;
- font-size: 12px;
- cursor: pointer;
- min-width: 42px;
- text-align: center;
- }
-
- .skm-use-btn:hover {
- background-color: #E67816;
- }
-
- .skm-footer {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- padding-top: 8px;
- border-top: 1px solid #E8DFD5;
- }
-
- .skm-version {
- font-size: 12px;
- color: #999 !important;
- margin-right: auto;
- align-self: center;
- }
-
- .skm-scrollbar {
- scrollbar-width: thin;
- scrollbar-color: #C96442 #FFF8F0;
- }
-
- .skm-scrollbar::-webkit-scrollbar {
- width: 6px;
- }
-
- .skm-scrollbar::-webkit-scrollbar-track {
- background: #FFF8F0;
- }
-
- .skm-scrollbar::-webkit-scrollbar-thumb {
- background-color: #C96442;
- border-radius: 6px;
- }
-
- .skm-hint {
- margin-bottom: 12px;
- padding: 8px;
- background-color: #FFF0DD !important;
- border-radius: 4px;
- border-left: 3px solid #C96442;
- }
-
- .skm-hint-text {
- font-size: 12px;
- color: #996633 !important;
- }
-
- /* Force correct colors in all themes */
- @media (prefers-color-scheme: light), (prefers-color-scheme: dark) {
- .skm-popup, .skm-popup-content {
- background-color: #FFF8F0 !important;
- }
-
- .skm-token-item {
- background-color: white !important;
- color: #333 !important;
- }
-
- .skm-token-name {
- color: #333 !important;
- }
-
- .skm-hint-text {
- color: #996633 !important;
- }
- }
- `;
- document.head.appendChild(styles);
-
- // =============== 创建主按钮 ===============
- const mainButton = document.createElement('button');
- mainButton.className = 'skm-main-button';
- mainButton.innerHTML = 'SK';
- mainButton.title = 'Claude SessionKey Manager';
- document.body.appendChild(mainButton);
-
- // =============== 创建弹出菜单 ===============
- const popup = document.createElement('div');
- popup.className = 'skm-popup';
-
- const popupContent = `
- <div class="skm-popup-header">
- <span>SessionKey Manager</span>
- <button class="skm-close-btn">×</button>
- </div>
- <div class="skm-popup-content">
- <div class="skm-hint">
- <div class="skm-hint-text">提示:鼠标长按 SK 图标可以拖动位置</div>
- </div>
- <div class="skm-token-list skm-scrollbar">
- ${tokens.map((token, index) => `
- <div class="skm-token-item" data-index="${index}">
- <div class="skm-token-name">${token.name}</div>
- <button class="skm-use-btn">使用</button>
- </div>
- `).join('')}
- </div>
- <div class="skm-footer">
- <div class="skm-version">v3.1</div>
- </div>
- </div>
- `;
-
- popup.innerHTML = popupContent;
- document.body.appendChild(popup);
-
- // =============== 功能实现 ===============
-
- // 加载保存的位置
- function loadSavedPosition() {
- const savedPosition = localStorage.getItem('skmButtonPosition');
- if (savedPosition) {
- try {
- const { top, right } = JSON.parse(savedPosition);
- mainButton.style.top = `${top}px`;
- mainButton.style.right = `${right}px`;
- } catch (error) {
- console.error('Failed to parse saved position', error);
- setDefaultPosition();
- }
- } else {
- setDefaultPosition();
- }
- }
-
- // 设置默认位置
- function setDefaultPosition() {
- mainButton.style.top = '70px';
- mainButton.style.right = '20px';
- }
-
- // 保存位置
- function savePosition() {
- const position = {
- top: parseInt(mainButton.style.top),
- right: parseInt(mainButton.style.right)
- };
- localStorage.setItem('skmButtonPosition', JSON.stringify(position));
- }
-
- // 拖拽功能实现
- let isDragging = false;
- let dragStartX, dragStartY;
- let initialRight, initialTop;
-
- function startDrag(e) {
- isDragging = true;
- mainButton.classList.add('skm-dragging');
-
- const rect = mainButton.getBoundingClientRect();
- initialRight = window.innerWidth - rect.right;
- initialTop = rect.top;
-
- if (e.type === 'touchstart') {
- dragStartX = e.touches[0].clientX;
- dragStartY = e.touches[0].clientY;
- } else {
- dragStartX = e.clientX;
- dragStartY = e.clientY;
- }
- }
-
- function handleMouseDown(e) {
- // 阻止默认行为以防止选中文本等
- e.preventDefault();
-
- // 直接开始拖拽,不加延迟
- startDrag(e);
-
- // 如果菜单是打开的,关闭它
- popup.classList.remove('active');
- }
-
- function handleMouseMove(e) {
- if (!isDragging) return;
- e.preventDefault();
-
- let currentX, currentY;
- if (e.type === 'touchmove') {
- currentX = e.touches[0].clientX;
- currentY = e.touches[0].clientY;
- } else {
- currentX = e.clientX;
- currentY = e.clientY;
- }
-
- const deltaX = currentX - dragStartX;
- const deltaY = currentY - dragStartY;
-
- const newRight = Math.max(10, initialRight - deltaX);
- const newTop = Math.max(10, initialTop + deltaY);
-
- // 限制不超出屏幕
- const maxRight = window.innerWidth - mainButton.offsetWidth - 10;
- const maxTop = window.innerHeight - mainButton.offsetHeight - 10;
-
- mainButton.style.right = `${Math.min(newRight, maxRight)}px`;
- mainButton.style.top = `${Math.min(newTop, maxTop)}px`;
- }
-
- function handleMouseUp(e) {
- if (!isDragging) return;
-
- e.preventDefault();
- isDragging = false;
- mainButton.classList.remove('skm-dragging');
- savePosition();
-
- // 防止点击事件同时触发
- e.stopPropagation();
- setTimeout(() => {
- isDragging = false;
- }, 0);
- }
-
- // 加载之前选择的token
- function loadSelectedToken() {
- const storedToken = localStorage.getItem('skmSelectedToken');
- if (storedToken) {
- const tokenItems = document.querySelectorAll('.skm-token-item');
- tokenItems.forEach(item => {
- const index = parseInt(item.dataset.index);
- if (tokens[index] && tokens[index].key === storedToken) {
- item.classList.add('active');
- }
- });
- }
- }
-
- // 处理token选择
- function handleTokenSelection(token) {
- if (token === '') {
- console.log('Empty token selected. No action taken.');
- } else {
- autoLogin(token);
- }
- }
-
- // 自动登录
- function autoLogin(token) {
- const currentURL = window.location.href;
- let loginUrl;
-
- if (currentURL.startsWith('https://demo.fuclaude.com/')) {
- loginUrl = `https://demo.fuclaude.com/login_token?session_key=${token}`;
- } else {
- loginUrl = `https://demo.fuclaude.com/login_token?session_key=${token}`;
- }
-
- window.location.href = loginUrl;
- }
-
- // 显示/隐藏弹出菜单
- function togglePopup(e) {
- // 如果正在拖拽,不触发菜单
- if (isDragging) return;
-
- // 阻止事件冒泡
- e.stopPropagation();
-
- const rect = mainButton.getBoundingClientRect();
- const isActive = popup.classList.contains('active');
-
- if (!isActive) {
- // 计算弹出位置,确保在屏幕内
- const windowWidth = window.innerWidth;
- const windowHeight = window.innerHeight;
-
- let left = rect.left;
- if (left + popup.offsetWidth > windowWidth - 10) {
- left = windowWidth - popup.offsetWidth - 10;
- }
-
- let top = rect.bottom + 10;
- // 如果下方空间不足,则显示在按钮上方
- if (top + popup.offsetHeight > windowHeight - 10) {
- top = rect.top - popup.offsetHeight - 10;
- }
-
- popup.style.left = `${Math.max(10, left)}px`;
- popup.style.top = `${Math.max(10, top)}px`;
-
- popup.classList.add('active');
- loadSelectedToken();
-
- // 点击外部关闭菜单
- setTimeout(() => {
- document.addEventListener('click', closePopupOnOutsideClick);
- }, 10);
- } else {
- popup.classList.remove('active');
- document.removeEventListener('click', closePopupOnOutsideClick);
- }
- }
-
- // 点击外部关闭菜单
- function closePopupOnOutsideClick(e) {
- if (!popup.contains(e.target) && e.target !== mainButton) {
- popup.classList.remove('active');
- document.removeEventListener('click', closePopupOnOutsideClick);
- }
- }
-
- // =============== 事件绑定 ===============
-
- // 主按钮点击事件
- mainButton.addEventListener('click', togglePopup);
-
- // 拖拽相关事件 - 鼠标
- mainButton.addEventListener('mousedown', handleMouseDown);
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
-
- // 拖拽相关事件 - 触摸
- mainButton.addEventListener('touchstart', handleMouseDown, { passive: false });
- document.addEventListener('touchmove', handleMouseMove, { passive: false });
- document.addEventListener('touchend', handleMouseUp);
-
- // ESC键关闭菜单
- document.addEventListener('keydown', e => {
- if (e.key === 'Escape') {
- popup.classList.remove('active');
- }
- });
-
- // 关闭按钮事件
- const closeBtn = popup.querySelector('.skm-close-btn');
- closeBtn.addEventListener('click', e => {
- e.stopPropagation();
- popup.classList.remove('active');
- document.removeEventListener('click', closePopupOnOutsideClick);
- });
-
- // Token选择事件
- const tokenItems = popup.querySelectorAll('.skm-token-item');
- tokenItems.forEach(item => {
- const useBtn = item.querySelector('.skm-use-btn');
-
- // 使用按钮点击事件
- useBtn.addEventListener('click', e => {
- e.stopPropagation();
- const index = parseInt(item.dataset.index);
- const selectedToken = tokens[index].key;
-
- // 更新选中状态
- tokenItems.forEach(i => i.classList.remove('active'));
- item.classList.add('active');
-
- // 保存选择并登录
- localStorage.setItem('skmSelectedToken', selectedToken);
- handleTokenSelection(selectedToken);
- });
-
- // 点击整个item也可以选择
- item.addEventListener('click', () => {
- const index = parseInt(item.dataset.index);
- const selectedToken = tokens[index].key;
-
- // 更新选中状态
- tokenItems.forEach(i => i.classList.remove('active'));
- item.classList.add('active');
-
- // 保存选择
- localStorage.setItem('skmSelectedToken', selectedToken);
- });
- });
-
- // 初始化位置
- loadSavedPosition();
- })();