Claude SessionKey Manager

轻松管理和切换Claude的SessionKey(可拖拽浮动按钮,简洁弹出式菜单)

当前为 2025-04-06 提交的版本,查看 最新版本

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