Claude SessionKey Manager

轻松管理和切换Claude的SessionKey(可拖拽浮动按钮,简洁弹出式菜单)。此脚本基于 https://greasyfork.org/scripts/501296,原作者: https://greasyfork.org/zh-CN/users/1337296

  1. // ==UserScript==
  2. // @name Claude SessionKey Manager
  3. // @version 3.1
  4. // @description 轻松管理和切换Claude的SessionKey(可拖拽浮动按钮,简洁弹出式菜单)。此脚本基于 https://greasyfork.org/scripts/501296,原作者: https://greasyfork.org/zh-CN/users/1337296
  5. // @match https://claude.ai/*
  6. // @match https://demo.fuclaude.com/*
  7. // @grant none
  8. // @license GNU GPLv3
  9. // @author f14xuanlv
  10. // @namespace https://greasyfork.org/users/1454591
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // =============== 配置项 ===============
  17. const tokens = [
  18. {name: 'default_empty', key: 'sk-ant-sid01-xxx'},
  19. {name: 'default_empty1', key: 'sk-ant-sid01-xxx'},
  20. // 此处添加更多的SessionKey
  21. ];
  22.  
  23. // =============== 样式设置 ===============
  24. const styles = document.createElement('style');
  25. styles.textContent = `
  26. .skm-main-button {
  27. position: fixed;
  28. z-index: 10000;
  29. display: flex;
  30. align-items: center;
  31. justify-content: center;
  32. width: 40px;
  33. height: 40px;
  34. border-radius: 50%;
  35. background: #C96442;
  36. color: white;
  37. font-size: 16px;
  38. font-weight: bold;
  39. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  40. cursor: pointer;
  41. user-select: none;
  42. touch-action: none;
  43. border: none;
  44. }
  45.  
  46. .skm-main-button:hover {
  47. background: #E67816;
  48. }
  49.  
  50. .skm-dragging {
  51. opacity: 0.8;
  52. }
  53.  
  54. .skm-popup {
  55. position: fixed;
  56. z-index: 9999;
  57. width: 280px;
  58. background-color: #FFF8F0 !important;
  59. border-radius: 8px;
  60. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  61. overflow: hidden;
  62. transition: opacity 0.2s ease;
  63. opacity: 0;
  64. pointer-events: none;
  65. }
  66.  
  67. .skm-popup.active {
  68. opacity: 1;
  69. pointer-events: all;
  70. }
  71.  
  72. .skm-popup-header {
  73. background: #C96442;
  74. color: white !important;
  75. padding: 12px 16px;
  76. font-weight: bold;
  77. font-size: 15px;
  78. display: flex;
  79. justify-content: space-between;
  80. align-items: center;
  81. }
  82.  
  83. .skm-close-btn {
  84. background: none;
  85. border: none;
  86. color: white !important;
  87. font-size: 18px;
  88. cursor: pointer;
  89. display: flex;
  90. align-items: center;
  91. justify-content: center;
  92. width: 24px;
  93. height: 24px;
  94. border-radius: 50%;
  95. }
  96.  
  97. .skm-close-btn:hover {
  98. background-color: rgba(255, 255, 255, 0.2);
  99. }
  100.  
  101. .skm-popup-content {
  102. padding: 16px;
  103. background-color: #FFF8F0 !important;
  104. }
  105.  
  106. .skm-token-list {
  107. margin-bottom: 16px;
  108. max-height: 200px;
  109. overflow-y: auto;
  110. }
  111.  
  112. .skm-token-item {
  113. padding: 10px;
  114. margin-bottom: 8px;
  115. border-radius: 6px;
  116. border: 1px solid #E8DFD5;
  117. background-color: white !important;
  118. display: flex;
  119. justify-content: space-between;
  120. align-items: center;
  121. cursor: pointer;
  122. transition: all 0.2s;
  123. color: #333 !important;
  124. }
  125.  
  126. .skm-token-item:hover {
  127. background-color: #FFF5EA !important;
  128. border-color: #C96442;
  129. color: #333 !important;
  130. }
  131.  
  132. .skm-token-item.active {
  133. background-color: #FFF0DD !important;
  134. border-color: #C96442;
  135. color: #333 !important;
  136. }
  137.  
  138. .skm-token-name {
  139. flex-grow: 1;
  140. font-size: 14px;
  141. padding-right: 8px;
  142. overflow: hidden;
  143. text-overflow: ellipsis;
  144. white-space: nowrap;
  145. color: #333 !important;
  146. }
  147.  
  148. .skm-use-btn {
  149. background-color: #C96442;
  150. color: white !important;
  151. border: none;
  152. border-radius: 4px;
  153. padding: 4px 8px;
  154. font-size: 12px;
  155. cursor: pointer;
  156. min-width: 42px;
  157. text-align: center;
  158. }
  159.  
  160. .skm-use-btn:hover {
  161. background-color: #E67816;
  162. }
  163.  
  164. .skm-footer {
  165. display: flex;
  166. justify-content: flex-end;
  167. gap: 8px;
  168. padding-top: 8px;
  169. border-top: 1px solid #E8DFD5;
  170. }
  171.  
  172. .skm-version {
  173. font-size: 12px;
  174. color: #999 !important;
  175. margin-right: auto;
  176. align-self: center;
  177. }
  178.  
  179. .skm-scrollbar {
  180. scrollbar-width: thin;
  181. scrollbar-color: #C96442 #FFF8F0;
  182. }
  183.  
  184. .skm-scrollbar::-webkit-scrollbar {
  185. width: 6px;
  186. }
  187.  
  188. .skm-scrollbar::-webkit-scrollbar-track {
  189. background: #FFF8F0;
  190. }
  191.  
  192. .skm-scrollbar::-webkit-scrollbar-thumb {
  193. background-color: #C96442;
  194. border-radius: 6px;
  195. }
  196.  
  197. .skm-hint {
  198. margin-bottom: 12px;
  199. padding: 8px;
  200. background-color: #FFF0DD !important;
  201. border-radius: 4px;
  202. border-left: 3px solid #C96442;
  203. }
  204.  
  205. .skm-hint-text {
  206. font-size: 12px;
  207. color: #996633 !important;
  208. }
  209.  
  210. /* Force correct colors in all themes */
  211. @media (prefers-color-scheme: light), (prefers-color-scheme: dark) {
  212. .skm-popup, .skm-popup-content {
  213. background-color: #FFF8F0 !important;
  214. }
  215. .skm-token-item {
  216. background-color: white !important;
  217. color: #333 !important;
  218. }
  219. .skm-token-name {
  220. color: #333 !important;
  221. }
  222. .skm-hint-text {
  223. color: #996633 !important;
  224. }
  225. }
  226. `;
  227. document.head.appendChild(styles);
  228.  
  229. // =============== 创建主按钮 ===============
  230. const mainButton = document.createElement('button');
  231. mainButton.className = 'skm-main-button';
  232. mainButton.innerHTML = 'SK';
  233. mainButton.title = 'Claude SessionKey Manager';
  234. document.body.appendChild(mainButton);
  235.  
  236. // =============== 创建弹出菜单 ===============
  237. const popup = document.createElement('div');
  238. popup.className = 'skm-popup';
  239. const popupContent = `
  240. <div class="skm-popup-header">
  241. <span>SessionKey Manager</span>
  242. <button class="skm-close-btn">&times;</button>
  243. </div>
  244. <div class="skm-popup-content">
  245. <div class="skm-hint">
  246. <div class="skm-hint-text">提示:鼠标长按 SK 图标可以拖动位置</div>
  247. </div>
  248. <div class="skm-token-list skm-scrollbar">
  249. ${tokens.map((token, index) => `
  250. <div class="skm-token-item" data-index="${index}">
  251. <div class="skm-token-name">${token.name}</div>
  252. <button class="skm-use-btn">使用</button>
  253. </div>
  254. `).join('')}
  255. </div>
  256. <div class="skm-footer">
  257. <div class="skm-version">v3.1</div>
  258. </div>
  259. </div>
  260. `;
  261. popup.innerHTML = popupContent;
  262. document.body.appendChild(popup);
  263.  
  264. // =============== 功能实现 ===============
  265. // 加载保存的位置
  266. function loadSavedPosition() {
  267. const savedPosition = localStorage.getItem('skmButtonPosition');
  268. if (savedPosition) {
  269. try {
  270. const { top, right } = JSON.parse(savedPosition);
  271. mainButton.style.top = `${top}px`;
  272. mainButton.style.right = `${right}px`;
  273. } catch (error) {
  274. console.error('Failed to parse saved position', error);
  275. setDefaultPosition();
  276. }
  277. } else {
  278. setDefaultPosition();
  279. }
  280. }
  281.  
  282. // 设置默认位置
  283. function setDefaultPosition() {
  284. mainButton.style.top = '70px';
  285. mainButton.style.right = '20px';
  286. }
  287.  
  288. // 保存位置
  289. function savePosition() {
  290. const position = {
  291. top: parseInt(mainButton.style.top),
  292. right: parseInt(mainButton.style.right)
  293. };
  294. localStorage.setItem('skmButtonPosition', JSON.stringify(position));
  295. }
  296.  
  297. // 拖拽功能实现
  298. let isDragging = false;
  299. let dragStartX, dragStartY;
  300. let initialRight, initialTop;
  301. function startDrag(e) {
  302. isDragging = true;
  303. mainButton.classList.add('skm-dragging');
  304. const rect = mainButton.getBoundingClientRect();
  305. initialRight = window.innerWidth - rect.right;
  306. initialTop = rect.top;
  307. if (e.type === 'touchstart') {
  308. dragStartX = e.touches[0].clientX;
  309. dragStartY = e.touches[0].clientY;
  310. } else {
  311. dragStartX = e.clientX;
  312. dragStartY = e.clientY;
  313. }
  314. }
  315.  
  316. function handleMouseDown(e) {
  317. // 阻止默认行为以防止选中文本等
  318. e.preventDefault();
  319. // 直接开始拖拽,不加延迟
  320. startDrag(e);
  321. // 如果菜单是打开的,关闭它
  322. popup.classList.remove('active');
  323. }
  324.  
  325. function handleMouseMove(e) {
  326. if (!isDragging) return;
  327. e.preventDefault();
  328. let currentX, currentY;
  329. if (e.type === 'touchmove') {
  330. currentX = e.touches[0].clientX;
  331. currentY = e.touches[0].clientY;
  332. } else {
  333. currentX = e.clientX;
  334. currentY = e.clientY;
  335. }
  336. const deltaX = currentX - dragStartX;
  337. const deltaY = currentY - dragStartY;
  338. const newRight = Math.max(10, initialRight - deltaX);
  339. const newTop = Math.max(10, initialTop + deltaY);
  340. // 限制不超出屏幕
  341. const maxRight = window.innerWidth - mainButton.offsetWidth - 10;
  342. const maxTop = window.innerHeight - mainButton.offsetHeight - 10;
  343. mainButton.style.right = `${Math.min(newRight, maxRight)}px`;
  344. mainButton.style.top = `${Math.min(newTop, maxTop)}px`;
  345. }
  346.  
  347. function handleMouseUp(e) {
  348. if (!isDragging) return;
  349. e.preventDefault();
  350. isDragging = false;
  351. mainButton.classList.remove('skm-dragging');
  352. savePosition();
  353. // 防止点击事件同时触发
  354. e.stopPropagation();
  355. setTimeout(() => {
  356. isDragging = false;
  357. }, 0);
  358. }
  359.  
  360. // 加载之前选择的token
  361. function loadSelectedToken() {
  362. const storedToken = localStorage.getItem('skmSelectedToken');
  363. if (storedToken) {
  364. const tokenItems = document.querySelectorAll('.skm-token-item');
  365. tokenItems.forEach(item => {
  366. const index = parseInt(item.dataset.index);
  367. if (tokens[index] && tokens[index].key === storedToken) {
  368. item.classList.add('active');
  369. }
  370. });
  371. }
  372. }
  373.  
  374. // 处理token选择
  375. function handleTokenSelection(token) {
  376. if (token === '') {
  377. console.log('Empty token selected. No action taken.');
  378. } else {
  379. autoLogin(token);
  380. }
  381. }
  382.  
  383. // 自动登录
  384. function autoLogin(token) {
  385. const currentURL = window.location.href;
  386. let loginUrl;
  387.  
  388. if (currentURL.startsWith('https://demo.fuclaude.com/')) {
  389. loginUrl = `https://demo.fuclaude.com/login_token?session_key=${token}`;
  390. } else {
  391. loginUrl = `https://demo.fuclaude.com/login_token?session_key=${token}`;
  392. }
  393.  
  394. window.location.href = loginUrl;
  395. }
  396.  
  397. // 显示/隐藏弹出菜单
  398. function togglePopup(e) {
  399. // 如果正在拖拽,不触发菜单
  400. if (isDragging) return;
  401. // 阻止事件冒泡
  402. e.stopPropagation();
  403. const rect = mainButton.getBoundingClientRect();
  404. const isActive = popup.classList.contains('active');
  405. if (!isActive) {
  406. // 计算弹出位置,确保在屏幕内
  407. const windowWidth = window.innerWidth;
  408. const windowHeight = window.innerHeight;
  409. let left = rect.left;
  410. if (left + popup.offsetWidth > windowWidth - 10) {
  411. left = windowWidth - popup.offsetWidth - 10;
  412. }
  413. let top = rect.bottom + 10;
  414. // 如果下方空间不足,则显示在按钮上方
  415. if (top + popup.offsetHeight > windowHeight - 10) {
  416. top = rect.top - popup.offsetHeight - 10;
  417. }
  418. popup.style.left = `${Math.max(10, left)}px`;
  419. popup.style.top = `${Math.max(10, top)}px`;
  420. popup.classList.add('active');
  421. loadSelectedToken();
  422. // 点击外部关闭菜单
  423. setTimeout(() => {
  424. document.addEventListener('click', closePopupOnOutsideClick);
  425. }, 10);
  426. } else {
  427. popup.classList.remove('active');
  428. document.removeEventListener('click', closePopupOnOutsideClick);
  429. }
  430. }
  431. // 点击外部关闭菜单
  432. function closePopupOnOutsideClick(e) {
  433. if (!popup.contains(e.target) && e.target !== mainButton) {
  434. popup.classList.remove('active');
  435. document.removeEventListener('click', closePopupOnOutsideClick);
  436. }
  437. }
  438.  
  439. // =============== 事件绑定 ===============
  440. // 主按钮点击事件
  441. mainButton.addEventListener('click', togglePopup);
  442. // 拖拽相关事件 - 鼠标
  443. mainButton.addEventListener('mousedown', handleMouseDown);
  444. document.addEventListener('mousemove', handleMouseMove);
  445. document.addEventListener('mouseup', handleMouseUp);
  446. // 拖拽相关事件 - 触摸
  447. mainButton.addEventListener('touchstart', handleMouseDown, { passive: false });
  448. document.addEventListener('touchmove', handleMouseMove, { passive: false });
  449. document.addEventListener('touchend', handleMouseUp);
  450. // ESC键关闭菜单
  451. document.addEventListener('keydown', e => {
  452. if (e.key === 'Escape') {
  453. popup.classList.remove('active');
  454. }
  455. });
  456. // 关闭按钮事件
  457. const closeBtn = popup.querySelector('.skm-close-btn');
  458. closeBtn.addEventListener('click', e => {
  459. e.stopPropagation();
  460. popup.classList.remove('active');
  461. document.removeEventListener('click', closePopupOnOutsideClick);
  462. });
  463. // Token选择事件
  464. const tokenItems = popup.querySelectorAll('.skm-token-item');
  465. tokenItems.forEach(item => {
  466. const useBtn = item.querySelector('.skm-use-btn');
  467. // 使用按钮点击事件
  468. useBtn.addEventListener('click', e => {
  469. e.stopPropagation();
  470. const index = parseInt(item.dataset.index);
  471. const selectedToken = tokens[index].key;
  472. // 更新选中状态
  473. tokenItems.forEach(i => i.classList.remove('active'));
  474. item.classList.add('active');
  475. // 保存选择并登录
  476. localStorage.setItem('skmSelectedToken', selectedToken);
  477. handleTokenSelection(selectedToken);
  478. });
  479. // 点击整个item也可以选择
  480. item.addEventListener('click', () => {
  481. const index = parseInt(item.dataset.index);
  482. const selectedToken = tokens[index].key;
  483. // 更新选中状态
  484. tokenItems.forEach(i => i.classList.remove('active'));
  485. item.classList.add('active');
  486. // 保存选择
  487. localStorage.setItem('skmSelectedToken', selectedToken);
  488. });
  489. });
  490.  
  491. // 初始化位置
  492. loadSavedPosition();
  493. })();