// ==UserScript==
// @name Claude Session Key 管理器
// @name:zh-CN Claude Session Key 管理器
// @name:en Claude Session Key Manager
// @version 1.0.1
// @description Claude Session Key 管理工具,支持拖拽、测活、导入导出、WebDAV云备份等功能
// @description:zh-CN Claude Session Key 管理工具,支持拖拽、测活、批量导入导出、WebDAV云备份等功能
// @description:en Claude Session Key Manager with drag-and-drop, token validation, import/export, WebDAV backup and more
// @author xiaoye6688
// @namespace https://greasyfork.org/users/1317128-xiaoye6688
// @homepage https://greasyfork.org/zh-CN/users/1317128-xiaoye6688
// @supportURL https://greasyfork.org/zh-CN/users/1317128-xiaoye6688
// @license MIT
// @date 2025-03-09
// @modified 2025-03-09
// @match https://claude.ai/*
// @match https://claude.asia/*
// @match https://demo.fuclaude.com/*
// @include https://*fuclaude*/*
//
// @icon https://claude.ai/favicon.ico
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_info
// @connect ipapi.co
// @connect api.claude.ai
// @connect *
// ==/UserScript==
(function () {
"use strict";
const config = {
storageKey: "claudeTokens",
ipApiUrl: "https://ipapi.co/country_code",
defaultToken: {
name: "Token00",
key: "sk-key",
},
currentTokenKey: "currentClaudeToken",
testResultsKey: "claudeTokenTestResults",
testResultExpiry: 1800000, // 30分钟过期
};
const theme = {
light: {
bgColor: "#fcfaf5",
textColor: "#333",
borderColor: "#ccc",
buttonBg: "#f5f1e9",
buttonHoverBg: "#e5e1d9",
modalBg: "rgba(0, 0, 0, 0.5)",
},
dark: {
bgColor: "#2c2b28",
textColor: "#f5f4ef",
borderColor: "#3f3f3c",
buttonBg: "#3f3f3c",
buttonHoverBg: "#4a4a47",
modalBg: "rgba(0, 0, 0, 0.7)",
},
};
const getStyles = (isDarkMode) => `
:root {
--bg-color: ${isDarkMode ? theme.dark.bgColor : theme.light.bgColor};
--text-color: ${isDarkMode ? theme.dark.textColor : theme.light.textColor};
--border-color: ${isDarkMode ? theme.dark.borderColor : theme.light.borderColor};
--button-bg: ${isDarkMode ? theme.dark.buttonBg : theme.light.buttonBg};
--button-hover-bg: ${isDarkMode ? theme.dark.buttonHoverBg : theme.light.buttonHoverBg};
--modal-bg: ${isDarkMode ? theme.dark.modalBg : theme.light.modalBg};
}
/* 浮动按钮样式 */
#claude-toggle-button {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--bg-color);
color: #b3462f;
cursor: move;
position: fixed;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease, transform 0.2s ease;
outline: none;
padding: 0;
user-select: none;
touch-action: none;
border: 1px solid var(--border-color);
font-size: 18px;
}
#claude-toggle-button:hover {
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
/* 下拉容器样式 */
.claude-dropdown-container {
position: fixed;
background-color: var(--bg-color);
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
display: none;
flex-direction: column;
gap: 0; /* 移除flex布局产生的空隙 */
width: 600px;
max-height: 80vh;
overflow-y: auto;
z-index: 9999;
border: 1px solid var(--border-color);
opacity: 0;
transform: scale(0.95);
transition: opacity 0.3s ease, transform 0.3s ease;
scrollbar-gutter: stable; /* 保持滚动条空间稳定 */
scrollbar-width: thin; /* Firefox滚动条样式 */
scrollbar-color: ${isDarkMode ? "rgba(255, 255, 255, 0.2) transparent" : "rgba(0, 0, 0, 0.2) transparent"};
}
/* 标题容器 */
.claude-title-container {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
}
.claude-title-container h2 {
margin: 0;
color: var(--text-color);
font-size: 18px;
font-weight: 600;
}
.claude-ip-display {
font-size: 14px;
color: var(--text-color);
padding: 4px 10px;
background-color: var(--button-bg);
border-radius: 12px;
}
/* Token 网格容器 */
.claude-token-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
max-height: calc(2 * (90px + 12px) + 24px); /* 两行token的高度加上间隙和padding */
overflow-y: auto;
padding: 12px 0 12px 12px;
scrollbar-gutter: stable; /* 保持滚动条空间稳定,防止出现时推动内容 */
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: ${isDarkMode ? "rgba(255, 255, 255, 0.03)" : "rgba(0, 0, 0, 0.02)"};
/* Firefox滚动条样式支持 */
scrollbar-width: thin;
scrollbar-color: ${isDarkMode ? "rgba(255, 255, 255, 0.2) transparent" : "rgba(0, 0, 0, 0.2) transparent"};
}
/* Token 卡片样式 */
.claude-token-item {
padding: 15px;
border-radius: 8px;
background-color: var(--bg-color);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
height: 90px; /* 固定高度 */
box-sizing: border-box; /* 确保padding不会增加总高度 */
display: flex;
flex-direction: column;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.claude-token-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: ${isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.2)"};
}
.claude-token-item.current-token {
border: 2px solid #b3462f;
background-color: ${isDarkMode ? "rgba(179, 70, 47, 0.1)" : "rgba(179, 70, 47, 0.05)"};
position: relative;
}
.current-token-badge {
position: absolute;
top: -8px;
left: 8px;
background-color: #b3462f;
width: 20px;
height: 20px;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.current-token-badge::after {
content: "";
display: block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: white;
}
/* Token 内容样式 */
.token-info {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1; /* 填充可用空间 */
justify-content: space-between; /* 顶部行和底部行分别位于容器顶部和底部 */
}
.token-top-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.token-name-container {
display: flex;
align-items: center;
gap: 8px;
}
.token-number {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
background-color: var(--button-bg);
}
.token-name {
font-weight: 500;
font-size: 14px;
}
.token-actions {
display: flex;
gap: 8px;
}
.token-action-btn {
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color);
transition: all 0.2s ease;
}
.token-action-btn:hover {
background-color: var(--button-hover-bg);
transform: scale(1.1);
}
.token-action-btn.delete-btn {
color: #e24a4a;
}
.token-bottom-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.token-status {
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #888;
}
.status-indicator.success {
background-color: #48bb78;
}
.status-indicator.error {
background-color: #e53e3e;
}
.status-indicator.loading {
background-color: #888;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 0.4; }
50% { opacity: 1; }
100% { opacity: 0.4; }
}
.token-time {
font-size: 12px;
color: var(--text-color);
opacity: 0.7;
}
/* 按钮容器 */
.claude-button-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding-top: 12px;
}
.claude-button {
padding: 8px 10px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
}
.claude-button:hover {
transform: translateY(-2px);
}
.claude-button.primary {
background-color: #b3462f;
color: white;
}
.claude-button.primary:hover {
background-color: #a03d2a;
}
.claude-button.secondary {
background-color: var(--button-bg);
color: var(--text-color);
}
.claude-button.secondary:hover {
background-color: var(--button-hover-bg);
}
/* 工具提示样式 */
.claude-button[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 10001;
margin-bottom: 5px;
pointer-events: none;
opacity: 0;
animation: tooltip-fade-in 0.2s ease forwards;
}
@keyframes tooltip-fade-in {
from { opacity: 0; transform: translate(-50%, 5px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
/* 模态框样式 */
.claude-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--modal-bg);
display: flex;
justify-content: center;
align-items: center;
z-index: 10001;
}
.claude-modal-content {
background-color: var(--bg-color);
padding: 20px;
padding-right: 14px; /* 右侧padding稍微增加,为滚动条预留空间但不过多 */
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 500px;
max-width: 90%;
overflow-y: auto;
position: relative;
scrollbar-gutter: stable; /* 保持滚动条空间稳定 */
scrollbar-width: thin; /* Firefox滚动条样式 */
scrollbar-color: ${isDarkMode ? "rgba(255, 255, 255, 0.2) transparent" : "rgba(0, 0, 0, 0.2) transparent"};
}
.claude-modal-content.narrow-modal {
width: 400px;
max-width: 80%;
}
.claude-modal h2 {
margin-top: 0;
margin-bottom: 15px;
color: var(--text-color);
font-size: 18px;
font-weight: 600;
}
.claude-modal input, .claude-modal textarea {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
background-color: var(--bg-color);
color: var(--text-color);
}
.claude-modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
}
.claude-close-button {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: var(--text-color);
padding: 5px;
line-height: 1;
}
/* 自定义滚动条样式 */
.claude-token-grid::-webkit-scrollbar {
width: 6px;
/* 初始状态下滚动条透明 */
background-color: transparent;
}
.claude-token-grid::-webkit-scrollbar-track {
background: transparent;
margin: 4px 0;
}
.claude-token-grid::-webkit-scrollbar-thumb {
background-color: ${isDarkMode ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.15)"};
border-radius: 6px;
transition: background-color 0.3s ease;
/* 初始状态下滚动条半透明 */
opacity: 0.6;
}
.claude-token-grid::-webkit-scrollbar-thumb:hover {
background-color: ${isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.3)"};
opacity: 1;
}
.claude-token-grid:hover::-webkit-scrollbar-thumb {
background-color: ${isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.3)"};
opacity: 1;
}
/* 滚动条样式 */
.claude-dropdown-container::-webkit-scrollbar,
.claude-modal-content::-webkit-scrollbar {
width: 6px;
background-color: transparent;
}
.claude-dropdown-container::-webkit-scrollbar-track,
.claude-modal-content::-webkit-scrollbar-track {
background: transparent;
margin: 4px 0;
}
.claude-dropdown-container::-webkit-scrollbar-thumb,
.claude-modal-content::-webkit-scrollbar-thumb {
background-color: ${isDarkMode ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.15)"};
border-radius: 6px;
transition: background-color 0.3s ease;
opacity: 0.6;
}
.claude-dropdown-container::-webkit-scrollbar-thumb:hover,
.claude-modal-content::-webkit-scrollbar-thumb:hover {
background-color: ${isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.3)"};
opacity: 1;
}
.claude-dropdown-container:hover::-webkit-scrollbar-thumb,
.claude-modal-content:hover::-webkit-scrollbar-thumb {
opacity: 1;
}
/* 预览容器 */
.claude-preview-container {
margin-top: 15px;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 15px;
}
.claude-preview-title {
font-size: 16px;
margin-bottom: 10px;
color: var(--text-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: 5px;
}
.claude-preview-item {
margin-bottom: 8px;
font-size: 14px;
padding: 8px;
border-radius: 4px;
background-color: var(--button-bg);
}
/* 滚动提示样式 */
.scroll-indicator {
grid-column: 1 / -1; /* 横跨所有列 */
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
margin-top: 5px;
color: ${isDarkMode ? "rgba(255, 255, 255, 0.6)" : "rgba(0, 0, 0, 0.5)"};
font-size: 12px;
background-color: ${isDarkMode ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)"};
border-radius: 6px;
gap: 8px;
}
.scroll-arrow {
animation: bounce 1.5s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(3px); }
}
`;
const UI = {
createElem(tag, className = "", styles = {}) {
const elem = document.createElement(tag);
if (className) elem.className = className;
Object.assign(elem.style, styles);
return elem;
},
createButton(text, className, icon = "") {
const button = this.createElem("button", className);
if (icon) {
button.innerHTML = `${icon} ${text}`;
} else {
button.textContent = text;
}
return button;
},
createModal(title, content, includeCloseButton = true) {
const modal = this.createElem("div", "claude-modal");
modal.setAttribute("aria-modal", "true");
modal.setAttribute("role", "dialog");
const modalContent = this.createElem("div", "claude-modal-content");
const titleElem = this.createElem("h2");
titleElem.textContent = title;
modalContent.appendChild(titleElem);
if (includeCloseButton) {
const closeButton = this.createElem("button", "claude-close-button");
closeButton.textContent = "×";
closeButton.addEventListener("click", () => document.body.removeChild(modal));
modalContent.appendChild(closeButton);
}
modalContent.appendChild(content);
const buttonContainer = this.createElem("div", "claude-modal-buttons");
modalContent.appendChild(buttonContainer);
modal.appendChild(modalContent);
document.body.appendChild(modal);
return {
modal,
buttonContainer,
close: () => document.body.removeChild(modal),
};
},
};
const App = {
init() {
this.isDarkMode = document.documentElement.getAttribute("data-mode") === "dark";
this.injectStyles();
this.tokens = this.loadTokens();
this.createUI();
this.setupEventListeners();
this.observeThemeChanges();
// 获取保存的位置或使用默认值
const savedPosition = {
left: GM_getValue("buttonLeft", 10),
bottom: GM_getValue("buttonBottom", 10)
};
// 设置按钮位置
this.toggleButton.style.left = `${savedPosition.left}px`;
this.toggleButton.style.bottom = `${savedPosition.bottom}px`;
// 初始化拖拽状态
this.isDragging = false;
this.buttonLeft = savedPosition.left;
this.buttonBottom = savedPosition.bottom;
// 获取IP信息
this.fetchIPCountryCode();
},
injectStyles() {
this.styleElem = document.createElement("style");
this.styleElem.textContent = getStyles(this.isDarkMode);
document.head.appendChild(this.styleElem);
},
updateStyles() {
this.styleElem.textContent = getStyles(this.isDarkMode);
},
loadTokens() {
try {
const savedTokens = GM_getValue(config.storageKey);
let tokens = savedTokens && savedTokens.length > 0
? savedTokens
: [config.defaultToken];
// 为没有创建时间的token添加默认值
tokens = tokens.map(token => {
if (!token.createdAt) {
const now = new Date();
return {
...token,
createdAt: now.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
timestamp: now.getTime()
};
}
return token;
});
return tokens;
} catch (error) {
console.error("加载 tokens 失败:", error);
return [config.defaultToken];
}
},
saveTokens() {
try {
GM_setValue(config.storageKey, this.tokens);
} catch (error) {
console.error("保存 tokens 失败:", error);
alert("保存 tokens 失败,请重试。");
}
},
createUI() {
// 创建浮动按钮
this.toggleButton = UI.createElem("button", "claude-toggle-button", {
left: "10px",
bottom: "10px"
});
this.toggleButton.id = "claude-toggle-button";
this.toggleButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" pointer-events="none"></path>
</svg>
`;
document.body.appendChild(this.toggleButton);
// 创建下拉容器
this.dropdownContainer = UI.createElem("div", "claude-dropdown-container");
document.body.appendChild(this.dropdownContainer);
// 创建标题容器
const titleContainer = UI.createElem("div", "claude-title-container");
const title = UI.createElem("h2");
title.textContent = "Claude Session Key 管理器";
this.ipDisplay = UI.createElem("div", "claude-ip-display");
this.ipDisplay.textContent = "IP: 加载中...";
titleContainer.appendChild(title);
titleContainer.appendChild(this.ipDisplay);
this.dropdownContainer.appendChild(titleContainer);
// 创建 Token 网格
this.tokenGrid = UI.createElem("div", "claude-token-grid");
this.dropdownContainer.appendChild(this.tokenGrid);
// 更新 Token 网格
this.updateTokenGrid();
// 创建按钮容器
const buttonContainer = UI.createElem("div", "claude-button-container");
// 测试所有按钮
const testAllButton = UI.createButton("测活", "claude-button primary", "🔍");
testAllButton.setAttribute("data-tooltip", "测试所有Token是否有效");
testAllButton.addEventListener("click", () => this.testAllTokens());
buttonContainer.appendChild(testAllButton);
// 清理无效按钮
const cleanInvalidButton = UI.createButton("清理", "claude-button secondary", "🗑️");
cleanInvalidButton.setAttribute("data-tooltip", "清理所有无效的Token");
cleanInvalidButton.addEventListener("click", () => this.removeInvalidTokens());
buttonContainer.appendChild(cleanInvalidButton);
// 添加 Token 按钮
const addTokenButton = UI.createButton("添加", "claude-button secondary", "➕");
addTokenButton.setAttribute("data-tooltip", "添加新的Token");
addTokenButton.addEventListener("click", () => this.showAddTokenModal());
buttonContainer.appendChild(addTokenButton);
// 批量导入按钮
const importButton = UI.createButton("导入", "claude-button secondary", "📥");
importButton.setAttribute("data-tooltip", "批量导入多个Token");
importButton.addEventListener("click", () => this.showBulkImportModal());
buttonContainer.appendChild(importButton);
// 批量导出按钮
const exportButton = UI.createButton("导出", "claude-button secondary", "📤");
exportButton.setAttribute("data-tooltip", "导出所有Token");
exportButton.addEventListener("click", () => this.exportTokens());
buttonContainer.appendChild(exportButton);
// WebDAV备份按钮
const webdavButton = UI.createButton("云备份", "claude-button secondary", "☁️");
webdavButton.setAttribute("data-tooltip", "WebDAV云备份与恢复");
webdavButton.addEventListener("click", () => this.showWebDAVModal());
buttonContainer.appendChild(webdavButton);
this.dropdownContainer.appendChild(buttonContainer);
// 添加信息提示
const infoSection = UI.createElem("div", "claude-info-section", {
marginTop: "10px",
padding: "8px",
backgroundColor: "#f8f9fa",
borderRadius: "6px",
fontSize: "11px",
color: "#666",
textAlign: "center"
});
infoSection.innerHTML = '悬停显示面板 • 拖拽按钮调整位置 • 支持WebDAV云备份';
this.dropdownContainer.appendChild(infoSection);
},
updateTokenGrid() {
this.tokenGrid.innerHTML = "";
// 获取当前使用的 token
const currentTokenName = GM_getValue(config.currentTokenKey);
// 加载测试结果
const testResults = this.loadTestResults();
this.tokens.forEach((token, index) => {
const tokenItem = UI.createElem("div", "claude-token-item");
if (token.name === currentTokenName) {
tokenItem.classList.add("current-token");
// 添加选中标记
const currentBadge = UI.createElem("div", "current-token-badge");
tokenItem.appendChild(currentBadge);
}
// Token 信息容器
const tokenInfo = UI.createElem("div", "token-info");
// 顶部行:名称和操作按钮
const topRow = UI.createElem("div", "token-top-row");
// 名称容器
const nameContainer = UI.createElem("div", "token-name-container");
const numberBadge = UI.createElem("span", "token-number");
numberBadge.textContent = `#${(index + 1).toString().padStart(2, "0")}`;
const nameSpan = UI.createElem("span", "token-name");
nameSpan.textContent = token.name;
nameContainer.appendChild(numberBadge);
nameContainer.appendChild(nameSpan);
// 操作按钮
const actions = UI.createElem("div", "token-actions");
// 编辑按钮
const editButton = UI.createElem("button", "token-action-btn edit-btn");
editButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
</svg>
`;
editButton.dataset.index = index;
editButton.addEventListener("click", (e) => {
e.stopPropagation();
this.showEditTokenModal(index);
});
// 删除按钮
const deleteButton = UI.createElem("button", "token-action-btn delete-btn");
deleteButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
`;
deleteButton.dataset.index = index;
deleteButton.addEventListener("click", (e) => {
e.stopPropagation();
this.confirmDeleteToken(index);
});
actions.appendChild(editButton);
actions.appendChild(deleteButton);
topRow.appendChild(nameContainer);
topRow.appendChild(actions);
// 底部行:状态和时间
const bottomRow = UI.createElem("div", "token-bottom-row");
// 添加时间戳(使用token的创建时间)
const timeSpan = UI.createElem("span", "token-time");
timeSpan.textContent = token.createdAt || "";
bottomRow.appendChild(timeSpan);
// 状态指示器
const status = UI.createElem("div", "token-status");
const statusIndicator = UI.createElem("div", "status-indicator");
// 检查缓存的测试结果
const testResult = testResults[token.key];
if (testResult) {
statusIndicator.classList.add(testResult.status);
}
status.appendChild(statusIndicator);
status.addEventListener("click", async (e) => {
e.stopPropagation();
await this.testSingleToken(token, statusIndicator, bottomRow);
});
bottomRow.appendChild(status);
// 将行添加到信息容器
tokenInfo.appendChild(topRow);
tokenInfo.appendChild(bottomRow);
// 将信息容器添加到 token 项
tokenItem.appendChild(tokenInfo);
// 点击切换 token
tokenItem.addEventListener("click", () => this.switchToToken(token));
// 将 token 项添加到网格
this.tokenGrid.appendChild(tokenItem);
});
// 如果token数量超过4个(两行),添加滚动提示
if (this.tokens.length > 4) {
const scrollIndicator = UI.createElem("div", "scroll-indicator");
scrollIndicator.innerHTML = `
<div class="scroll-text">向下滚动查看更多 (${this.tokens.length - 4})</div>
<div class="scroll-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 13l5 5 5-5"></path>
<path d="M7 6l5 5 5-5"></path>
</svg>
</div>
`;
this.tokenGrid.appendChild(scrollIndicator);
}
},
async switchToToken(token) {
// 检查是否有缓存的测试结果
const cachedResult = this.getTestResult(token.key);
// 如果有缓存的测试结果且为无效,提示用户并询问是否继续
if (cachedResult && cachedResult.status === "error") {
const confirmResult = await this.showConfirmDialog(
"警告",
`该 Token "${token.name}" 已被标记为无效,是否仍要切换到该 Token?`
);
if (!confirmResult) {
return;
}
}
// 应用 token
this.applyToken(token.key);
GM_setValue(config.currentTokenKey, token.name);
// 隐藏下拉菜单
this.hideDropdown();
},
applyToken(token) {
const currentURL = window.location.href;
if (currentURL.startsWith("https://claude.ai/")) {
document.cookie = `sessionKey=${token}; path=/; domain=.claude.ai`;
window.location.reload();
} else {
let loginUrl;
const hostname = new URL(currentURL).hostname;
if (hostname !== "claude.ai") {
loginUrl = `https://${hostname}/login_token?session_key=${token}`;
}
if (loginUrl) {
window.location.href = loginUrl;
}
}
},
async testSingleToken(token, statusIndicator, bottomRow) {
// 显示加载状态
statusIndicator.className = "status-indicator loading";
// 测试 token
const result = await this.testToken(token.key);
// 保存测试结果
this.saveTestResult(token.key, result);
// 更新状态指示器
statusIndicator.className = `status-indicator ${result.status}`;
// 不再更新时间戳,保持显示token的创建时间
},
async testAllTokens() {
// 获取所有 token 项
const tokenItems = this.tokenGrid.querySelectorAll(".claude-token-item");
// 禁用测试按钮
const testButton = this.dropdownContainer.querySelector(".claude-button.primary");
testButton.disabled = true;
testButton.textContent = "测试中...";
// 清除所有缓存的测试结果
GM_setValue(config.testResultsKey, {});
const tokens = Array.from(tokenItems);
// 按4个一组处理所有tokens
for (let i = 0; i < tokens.length; i += 4) {
// 取出当前4个(或更少)token
const currentChunk = tokens.slice(i, Math.min(i + 4, tokens.length));
// 并行处理这最多4个token
await Promise.all(
currentChunk.map(async (tokenItem) => {
const index = Array.from(tokenItems).indexOf(tokenItem);
const token = this.tokens[index];
const statusIndicator = tokenItem.querySelector(".status-indicator");
const bottomRow = tokenItem.querySelector(".token-bottom-row");
await this.testSingleToken(token, statusIndicator, bottomRow);
})
);
}
// 恢复测试按钮
testButton.disabled = false;
testButton.innerHTML = "🔍 测活";
},
async testToken(key) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: "https://api.claude.ai/api/organizations",
headers: {
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept-language": "en-US,en;q=0.9",
"cache-control": "max-age=0",
cookie: `sessionKey=${key}`,
"user-agent": "Mozilla/5.0 (X11; Linux x86_64)",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
},
onload: (response) => {
try {
if (response.status !== 200) {
resolve({ status: "error", message: "无效" });
return;
}
const responseText = response.responseText;
if (responseText.toLowerCase().includes("unauthorized")) {
resolve({ status: "error", message: "无效" });
return;
}
if (responseText.trim() === "") {
resolve({ status: "error", message: "无响应" });
return;
}
try {
const objects = JSON.parse(responseText);
if (objects && objects.length > 0) {
resolve({ status: "success", message: "有效" });
return;
}
} catch (e) {
resolve({ status: "error", message: "解析失败" });
return;
}
resolve({ status: "error", message: "无效数据" });
} catch (error) {
console.error("解析响应时发生错误:", error);
resolve({ status: "error", message: "测试失败" });
}
},
onerror: (error) => {
console.error("请求发生错误:", error);
resolve({ status: "error", message: "网络错误" });
},
ontimeout: () => {
resolve({ status: "error", message: "超时" });
},
});
});
},
loadTestResults() {
try {
const cached = GM_getValue(config.testResultsKey, {});
const now = Date.now();
// 清理过期的测试结果
const filtered = Object.entries(cached).reduce((acc, [key, value]) => {
if (now - value.timestamp < config.testResultExpiry) {
acc[key] = value;
}
return acc;
}, {});
return filtered;
} catch (error) {
console.error("加载测试结果缓存失败:", error);
return {};
}
},
saveTestResult(key, result) {
try {
const testResults = this.loadTestResults();
const now = new Date();
// 统一使用简短时间格式
const formattedTime = now.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
testResults[key] = {
status: result.status,
message: result.message,
timestamp: now.getTime(),
testTime: formattedTime, // 保存简短格式的时间
};
GM_setValue(config.testResultsKey, testResults);
} catch (error) {
console.error("保存测试结果失败:", error);
}
},
getTestResult(key) {
const testResults = this.loadTestResults();
return testResults[key];
},
async removeInvalidTokens() {
const confirmResult = await this.showConfirmDialog(
"确认清理",
"是否删除所有无效的 Tokens?此操作不可撤销。"
);
if (!confirmResult) return;
const testResults = this.loadTestResults();
const validTokens = this.tokens.filter((token) => {
const result = testResults[token.key];
return !result || result.status === "success";
});
if (validTokens.length === this.tokens.length) {
alert("没有发现无效的 Tokens");
return;
}
this.tokens = validTokens;
this.saveTokens();
this.updateTokenGrid();
},
showAddTokenModal() {
const content = UI.createElem("div", "claude-add-token-form");
const nameInput = UI.createElem("input");
nameInput.placeholder = "Token 名称";
nameInput.setAttribute("aria-label", "Token 名称");
const keyInput = UI.createElem("input");
keyInput.placeholder = "Token 密钥";
keyInput.setAttribute("aria-label", "Token 密钥");
content.appendChild(nameInput);
content.appendChild(keyInput);
const { modal, buttonContainer, close } = UI.createModal("添加 Token", content);
modal.querySelector(".claude-modal-content").classList.add("narrow-modal");
const addButton = UI.createButton("添加", "claude-button primary");
addButton.addEventListener("click", () => {
if (this.validateInput(nameInput.value, keyInput.value)) {
// 获取当前时间并格式化
const now = new Date();
const formattedTime = now.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
this.tokens.push({
name: nameInput.value,
key: keyInput.value,
createdAt: formattedTime, // 添加创建时间
timestamp: now.getTime() // 添加时间戳用于排序
});
this.saveTokens();
this.updateTokenGrid();
close();
}
});
const cancelButton = UI.createButton("取消", "claude-button secondary");
cancelButton.addEventListener("click", close);
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(addButton);
},
showEditTokenModal(index) {
const token = this.tokens[index];
const content = UI.createElem("div", "claude-edit-token-form");
const nameInput = UI.createElem("input");
nameInput.value = token.name;
nameInput.placeholder = "Token 名称";
const keyInput = UI.createElem("input");
keyInput.value = token.key;
keyInput.placeholder = "Token 密钥";
content.appendChild(nameInput);
content.appendChild(keyInput);
const { modal, buttonContainer, close } = UI.createModal("编辑 Token", content);
modal.querySelector(".claude-modal-content").classList.add("narrow-modal");
const saveButton = UI.createButton("保存", "claude-button primary");
saveButton.addEventListener("click", () => {
if (this.validateInput(nameInput.value, keyInput.value)) {
// 保留原有的创建时间和时间戳
this.tokens[index] = {
name: nameInput.value,
key: keyInput.value,
createdAt: token.createdAt || "", // 保留原有的创建时间
timestamp: token.timestamp || Date.now() // 保留原有的时间戳
};
this.saveTokens();
this.updateTokenGrid();
close();
}
});
const cancelButton = UI.createButton("取消", "claude-button secondary");
cancelButton.addEventListener("click", close);
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(saveButton);
},
confirmDeleteToken(index) {
const token = this.tokens[index];
const content = UI.createElem("div", "claude-delete-confirm");
content.innerHTML = `
<div style="font-size: 48px; text-align: center; margin-bottom: 16px;">⚠️</div>
<div style="font-size: 18px; font-weight: 600; text-align: center; margin-bottom: 12px;">删除确认</div>
<div style="text-align: center; margin-bottom: 24px;">
您确定要删除 Token "${token.name}" 吗?<br>
此操作无法撤销。
</div>
`;
const { modal, buttonContainer, close } = UI.createModal("", content);
modal.querySelector(".claude-modal-content").classList.add("narrow-modal");
const deleteButton = UI.createButton("删除", "claude-button primary");
deleteButton.style.backgroundColor = "#e53e3e";
deleteButton.addEventListener("click", () => {
this.deleteToken(index);
close();
});
const cancelButton = UI.createButton("取消", "claude-button secondary");
cancelButton.addEventListener("click", close);
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(deleteButton);
},
deleteToken(index) {
this.tokens.splice(index, 1);
this.saveTokens();
this.updateTokenGrid();
},
showBulkImportModal() {
const content = UI.createElem("div", "claude-bulk-import-form");
// 文本区域标签
const textareaLabel = UI.createElem("label");
textareaLabel.innerHTML = "<strong>1️⃣ Tokens 粘贴区:</strong><br>在这里粘贴您需要导入的 Tokens,每行一个!";
content.appendChild(textareaLabel);
// 文本区域
const textarea = UI.createElem("textarea");
textarea.rows = 10;
content.appendChild(textarea);
// 命名规则容器
const namingRuleContainer = UI.createElem("div", "claude-naming-rule");
// 命名规则标签
const namingRuleLabel = UI.createElem("label");
namingRuleLabel.innerHTML = "<strong>2️⃣ Tokens 命名规则:</strong>";
namingRuleContainer.appendChild(namingRuleLabel);
// 名称前缀
const prefixLabel = UI.createElem("label");
prefixLabel.textContent = "名称前缀:";
namingRuleContainer.appendChild(prefixLabel);
const prefixInput = UI.createElem("input");
prefixInput.value = "token";
namingRuleContainer.appendChild(prefixInput);
// 起始编号
const startNumberLabel = UI.createElem("label");
startNumberLabel.textContent = "名称起始编号:";
namingRuleContainer.appendChild(startNumberLabel);
const startNumberInput = UI.createElem("input");
startNumberInput.type = "number";
startNumberInput.value = "1";
namingRuleContainer.appendChild(startNumberInput);
content.appendChild(namingRuleContainer);
// 预览容器
const previewLabel = UI.createElem("label");
previewLabel.innerHTML = "<strong>3️⃣ Tokens 导入结果预览:</strong>";
content.appendChild(previewLabel);
const previewContainer = UI.createElem("div", "claude-preview-container");
content.appendChild(previewContainer);
const { modal, buttonContainer, close } = UI.createModal("批量导入 Tokens", content);
const importButton = UI.createButton("导入", "claude-button primary");
importButton.addEventListener("click", () => {
this.performBulkImport(
textarea.value,
prefixInput.value,
startNumberInput.value
);
close();
});
const cancelButton = UI.createButton("取消", "claude-button secondary");
cancelButton.addEventListener("click", close);
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(importButton);
// 更新预览
const updatePreview = () => {
this.previewBulkImport(
textarea.value,
prefixInput.value,
startNumberInput.value,
previewContainer
);
};
[textarea, prefixInput, startNumberInput].forEach((elem) => {
elem.addEventListener("input", updatePreview);
});
// 初始化预览
updatePreview();
},
previewBulkImport(input, namePrefix, startNumber, previewContainer) {
previewContainer.innerHTML = "";
const tokens = this.parseTokens(input);
const namedTokens = this.applyNamingRule(
tokens,
namePrefix,
parseInt(startNumber)
);
const previewTitle = UI.createElem("div", "claude-preview-title");
previewTitle.textContent = "请核对下方导入结果:";
previewContainer.appendChild(previewTitle);
if (namedTokens.length === 0) {
const emptyMessage = UI.createElem("div", "claude-preview-item");
emptyMessage.textContent = "等待输入...";
previewContainer.appendChild(emptyMessage);
return;
}
namedTokens.forEach((token) => {
const previewItem = UI.createElem("div", "claude-preview-item");
previewItem.innerHTML = `
<strong>${token.name}:</strong>
<span style="font-family: monospace; word-break: break-all;">${token.key}</span>
`;
previewContainer.appendChild(previewItem);
});
},
performBulkImport(input, namePrefix, startNumber) {
const tokens = this.parseTokens(input);
const namedTokens = this.applyNamingRule(
tokens,
namePrefix,
parseInt(startNumber)
);
if (namedTokens.length === 0) {
alert("没有有效的 Tokens 可导入");
return;
}
this.tokens = [...this.tokens, ...namedTokens];
this.saveTokens();
this.updateTokenGrid();
},
parseTokens(input) {
return input
.split("\n")
.map((line) => line.trim())
.filter((line) => this.validateTokenKey(line))
.map((key) => ({ key }));
},
applyNamingRule(tokens, namePrefix, startNumber) {
return tokens.map((token, index) => {
const number = startNumber + index;
const name = `${namePrefix}${number.toString().padStart(2, "0")}`;
return { ...token, name };
});
},
exportTokens() {
const testResults = this.loadTestResults();
const exportData = this.tokens.map((token) => {
const testResult = testResults[token.key] || {};
return {
name: token.name,
sessionKey: token.key,
isValid: testResult.status === "success" ? true : testResult.status === "error" ? false : null,
testTime: testResult.testTime || null,
testMessage: testResult.message || null,
};
});
// 创建并下载 JSON 文件
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `claude_tokens_${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
showWebDAVModal() {
const content = UI.createElem("div", "claude-webdav-form");
content.style.cssText = "width: 100%; max-width: 600px;";
// 添加帮助信息
const helpInfo = UI.createElem("div", "claude-webdav-help");
helpInfo.style.cssText = `
margin-bottom: 10px;
padding: 12px;
background-color: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 13px;
color: var(--text-color);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
`;
helpInfo.innerHTML = `
<p style="margin: 0 0 10px 0; font-weight: 600; color: var(--text-color);">📝 WebDAV服务器设置说明:</p>
<ul style="margin: 0; padding-left: 20px; line-height: 1.6;">
<li>URL必须是完整的WebDAV路径,例如:https://dav.jianguoyun.com/dav/Claude/</li>
<li>确保路径末尾有斜杠"/"</li>
<li>如果遇到404错误,请确认路径是否存在</li>
<li>坚果云用户请使用应用专用密码</li>
</ul>
`;
content.appendChild(helpInfo);
// 创建表单容器
const formContainer = UI.createElem("div", "claude-webdav-form-container");
formContainer.style.cssText = `
display: grid;
gap: 8px;
margin-bottom: 15px;
`;
// WebDAV服务器URL输入
const urlGroup = UI.createElem("div", "input-group");
urlGroup.style.cssText = "display: flex; align-items: center; gap: 10px;";
const urlLabel = UI.createElem("label");
urlLabel.textContent = "WebDAV URL:";
urlLabel.style.cssText = "font-weight: 500; color: var(--text-color); min-width: 120px; text-align: right;";
const urlInput = UI.createElem("input");
urlInput.type = "text";
urlInput.placeholder = "https://dav.jianguoyun.com/dav/Claude/";
urlInput.value = GM_getValue("webdav_url", "");
urlInput.style.cssText = `
flex: 1;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background-color: var(--bg-color);
color: var(--text-color);
transition: all 0.3s ease;
`;
urlGroup.appendChild(urlLabel);
urlGroup.appendChild(urlInput);
formContainer.appendChild(urlGroup);
// 用户名输入
const usernameGroup = UI.createElem("div", "input-group");
usernameGroup.style.cssText = "display: flex; align-items: center; gap: 10px;";
const usernameLabel = UI.createElem("label");
usernameLabel.textContent = "用户名:";
usernameLabel.style.cssText = "font-weight: 500; color: var(--text-color); min-width: 120px; text-align: right;";
const usernameInput = UI.createElem("input");
usernameInput.type = "text";
usernameInput.placeholder = "用户名";
usernameInput.value = GM_getValue("webdav_username", "");
usernameInput.style.cssText = `
flex: 1;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background-color: var(--bg-color);
color: var(--text-color);
transition: all 0.3s ease;
`;
usernameGroup.appendChild(usernameLabel);
usernameGroup.appendChild(usernameInput);
formContainer.appendChild(usernameGroup);
// 密码输入
const passwordGroup = UI.createElem("div", "input-group");
passwordGroup.style.cssText = "display: flex; align-items: center; gap: 10px;";
const passwordLabel = UI.createElem("label");
passwordLabel.textContent = "密码:";
passwordLabel.style.cssText = "font-weight: 500; color: var(--text-color); min-width: 120px; text-align: right;";
const passwordInput = UI.createElem("input");
passwordInput.type = "password";
passwordInput.placeholder = "密码";
passwordInput.value = GM_getValue("webdav_password", "");
passwordInput.style.cssText = `
flex: 1;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background-color: var(--bg-color);
color: var(--text-color);
transition: all 0.3s ease;
`;
passwordGroup.appendChild(passwordLabel);
passwordGroup.appendChild(passwordInput);
formContainer.appendChild(passwordGroup);
// 文件名输入
const filenameGroup = UI.createElem("div", "input-group");
filenameGroup.style.cssText = "display: flex; align-items: center; gap: 10px;";
const filenameLabel = UI.createElem("label");
filenameLabel.textContent = "文件名:";
filenameLabel.style.cssText = "font-weight: 500; color: var(--text-color); min-width: 120px; text-align: right;";
const filenameInput = UI.createElem("input");
filenameInput.type = "text";
filenameInput.placeholder = "claude_tokens.json";
filenameInput.value = GM_getValue("webdav_filename", "claude_tokens.json");
filenameInput.style.cssText = `
flex: 1;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background-color: var(--bg-color);
color: var(--text-color);
transition: all 0.3s ease;
`;
filenameGroup.appendChild(filenameLabel);
filenameGroup.appendChild(filenameInput);
formContainer.appendChild(filenameGroup);
content.appendChild(formContainer);
// 测试连接按钮
const testConnectionButton = UI.createButton("测试连接", "claude-button secondary");
testConnectionButton.style.cssText = `
width: 100%;
margin: 10px 0;
padding: 10px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
background-color: var(--button-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.3s ease;
`;
testConnectionButton.addEventListener("click", async () => {
this.saveWebDAVSettings(urlInput.value, usernameInput.value, passwordInput.value, filenameInput.value);
statusDisplay.style.display = "block";
statusDisplay.textContent = "正在测试连接...";
statusDisplay.style.backgroundColor = "var(--bg-color)";
try {
await this.checkWebDAVDirectory(urlInput.value, usernameInput.value, passwordInput.value);
statusDisplay.textContent = "✅ 连接成功!目录存在且可访问。";
statusDisplay.style.backgroundColor = "#d4edda";
} catch (error) {
statusDisplay.textContent = `❌ 连接失败: ${error.message}`;
statusDisplay.style.backgroundColor = "#f8d7da";
}
});
content.appendChild(testConnectionButton);
// 状态显示
const statusDisplay = UI.createElem("div", "claude-webdav-status");
statusDisplay.style.cssText = `
margin: 10px 0;
padding: 10px;
border-radius: 6px;
font-size: 14px;
text-align: center;
display: none;
transition: all 0.3s ease;
`;
content.appendChild(statusDisplay);
// 操作按钮容器
const actionsContainer = UI.createElem("div", "claude-webdav-actions");
actionsContainer.style.cssText = `
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 15px;
`;
// 备份按钮
const backupButton = UI.createButton("备份到WebDAV", "claude-button primary");
backupButton.style.cssText = `
padding: 12px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
background-color: #b3462f;
color: white;
border: none;
cursor: pointer;
transition: all 0.3s ease;
`;
backupButton.addEventListener("click", async () => {
this.saveWebDAVSettings(urlInput.value, usernameInput.value, passwordInput.value, filenameInput.value);
statusDisplay.style.display = "block";
statusDisplay.textContent = "正在备份...";
statusDisplay.style.backgroundColor = "var(--bg-color)";
try {
await this.backupToWebDAV(urlInput.value, usernameInput.value, passwordInput.value, filenameInput.value);
statusDisplay.textContent = "✅ 备份成功!";
statusDisplay.style.backgroundColor = "#d4edda";
} catch (error) {
statusDisplay.textContent = `❌ 备份失败: ${error.message}`;
statusDisplay.style.backgroundColor = "#f8d7da";
}
});
// 恢复按钮
const restoreButton = UI.createButton("从WebDAV恢复", "claude-button secondary");
restoreButton.style.cssText = `
padding: 12px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
background-color: var(--button-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.3s ease;
`;
restoreButton.addEventListener("click", async () => {
this.saveWebDAVSettings(urlInput.value, usernameInput.value, passwordInput.value, filenameInput.value);
statusDisplay.style.display = "block";
statusDisplay.textContent = "正在恢复...";
statusDisplay.style.backgroundColor = "var(--bg-color)";
try {
await this.restoreFromWebDAV(urlInput.value, usernameInput.value, passwordInput.value, filenameInput.value);
statusDisplay.textContent = "✅ 恢复成功!";
statusDisplay.style.backgroundColor = "#d4edda";
this.updateTokenGrid();
} catch (error) {
statusDisplay.textContent = `❌ 恢复失败: ${error.message}`;
statusDisplay.style.backgroundColor = "#f8d7da";
}
});
// 关闭按钮
const closeButton = UI.createButton("关闭", "claude-button secondary");
closeButton.style.cssText = `
padding: 12px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
background-color: var(--button-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.3s ease;
`;
closeButton.addEventListener("click", () => {
modal.remove();
});
actionsContainer.appendChild(backupButton);
actionsContainer.appendChild(restoreButton);
actionsContainer.appendChild(closeButton);
content.appendChild(actionsContainer);
// 创建模态框
const { modal, buttonContainer } = UI.createModal("☁️ WebDAV备份与恢复", content, true);
document.body.appendChild(modal);
// 添加关闭按钮事件监听
const closeBtn = modal.querySelector(".claude-close-button");
if (closeBtn) {
closeBtn.addEventListener("click", () => {
document.body.removeChild(modal);
});
}
},
saveWebDAVSettings(url, username, password, filename) {
GM_setValue("webdav_url", url);
GM_setValue("webdav_username", username);
GM_setValue("webdav_password", password);
GM_setValue("webdav_filename", filename);
},
async backupToWebDAV(url, username, password, filename) {
// 准备备份数据
const testResults = this.loadTestResults();
const exportData = this.tokens.map((token) => {
const testResult = testResults[token.key] || {};
return {
name: token.name,
sessionKey: token.key,
isValid: testResult.status === "success" ? true : testResult.status === "error" ? false : null,
testTime: testResult.testTime || null,
testMessage: testResult.message || null,
};
});
const jsonData = JSON.stringify(exportData, null, 2);
// 确保URL以/结尾
if (!url.endsWith('/')) {
url += '/';
}
// 先检查目录是否存在
try {
await this.checkWebDAVDirectory(url, username, password);
} catch (error) {
// 如果是404错误,尝试创建目录
if (error.message.includes("404")) {
try {
// 尝试创建父目录
const parentUrl = url.substring(0, url.lastIndexOf('/', url.length - 2) + 1);
if (parentUrl !== url) {
await this.createWebDAVDirectory(parentUrl, username, password, url.substring(parentUrl.length, url.length - 1));
} else {
throw new Error("无法确定父目录");
}
} catch (createError) {
throw new Error(`目录不存在且无法创建: ${createError.message}`);
}
} else {
throw error;
}
}
// 发送到WebDAV服务器
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "PUT",
url: url + filename,
headers: {
"Content-Type": "application/json",
"Authorization": "Basic " + btoa(username + ":" + password)
},
data: jsonData,
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
resolve();
} else {
console.error("WebDAV备份失败:", response);
reject(new Error(`HTTP错误: ${response.status} ${response.statusText || ''}\n响应: ${response.responseText || '无响应内容'}`));
}
},
onerror: function (error) {
console.error("WebDAV备份网络错误:", error);
reject(new Error(`网络错误: ${error.statusText || '连接失败'}`));
}
});
});
},
// 检查WebDAV目录是否存在
checkWebDAVDirectory(url, username, password) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "PROPFIND",
url: url,
headers: {
"Depth": "0",
"Authorization": "Basic " + btoa(username + ":" + password)
},
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
resolve();
} else {
reject(new Error(`HTTP错误: ${response.status} ${response.statusText || ''}`));
}
},
onerror: function (error) {
reject(new Error(`网络错误: ${error.statusText || '连接失败'}`));
}
});
});
},
// 创建WebDAV目录
createWebDAVDirectory(parentUrl, username, password, dirName) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "MKCOL",
url: parentUrl + dirName,
headers: {
"Authorization": "Basic " + btoa(username + ":" + password)
},
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
resolve();
} else {
reject(new Error(`无法创建目录: HTTP错误 ${response.status} ${response.statusText || ''}`));
}
},
onerror: function (error) {
reject(new Error(`网络错误: ${error.statusText || '连接失败'}`));
}
});
});
},
async restoreFromWebDAV(url, username, password, filename) {
// 确保URL以/结尾
if (!url.endsWith('/')) {
url += '/';
}
// 从WebDAV服务器获取数据
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url + filename,
headers: {
"Authorization": "Basic " + btoa(username + ":" + password)
},
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
// 转换数据格式
const tokens = data.map(item => ({
name: item.name,
key: item.sessionKey,
createdAt: new Date().toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
timestamp: Date.now()
}));
// 更新tokens
this.tokens = tokens;
this.saveTokens();
resolve();
} catch (error) {
console.error("解析WebDAV数据失败:", error, response.responseText);
reject(new Error(`解析数据失败: ${error.message}`));
}
} else {
console.error("WebDAV恢复失败:", response);
reject(new Error(`HTTP错误: ${response.status} ${response.statusText || ''}\n响应: ${response.responseText || '无响应内容'}`));
}
},
onerror: function (error) {
console.error("WebDAV恢复网络错误:", error);
reject(new Error(`网络错误: ${error.statusText || '连接失败'}`));
}
});
});
},
validateInput(name, key) {
if (!name || !key) {
alert("Token 名称和密钥都要填写!");
return false;
}
// 移除对token名称的严格限制,允许更多字符,包括@和.
// if (!/^[a-zA-Z0-9-_]+$/.test(name)) {
// alert("Token 名称只能包含字母、数字、下划线和连字符!");
// return false;
// }
if (!this.validateTokenKey(key)) {
alert("无效的 Token 密钥格式!");
return false;
}
return true;
},
validateTokenKey(key) {
return /^sk-ant-sid\d{2}-[A-Za-z0-9_-]*$/.test(key);
},
showConfirmDialog(title, message) {
return new Promise((resolve) => {
const content = UI.createElem("div", "claude-confirm-dialog");
content.innerHTML = `
<div style="font-size: 48px; text-align: center; margin-bottom: 16px;">⚠️</div>
<div style="font-size: 18px; font-weight: 600; text-align: center; margin-bottom: 12px;">${title}</div>
<div style="text-align: center; margin-bottom: 24px;">${message}</div>
`;
const { modal, buttonContainer, close } = UI.createModal("", content);
modal.querySelector(".claude-modal-content").classList.add("narrow-modal");
const confirmButton = UI.createButton("确认", "claude-button primary");
confirmButton.addEventListener("click", () => {
close();
resolve(true);
});
const cancelButton = UI.createButton("取消", "claude-button secondary");
cancelButton.addEventListener("click", () => {
close();
resolve(false);
});
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(confirmButton);
});
},
fetchIPCountryCode() {
this.ipDisplay.textContent = "IP: 加载中...";
GM_xmlhttpRequest({
method: "GET",
url: config.ipApiUrl,
onload: (response) => {
if (response.status === 200) {
this.ipDisplay.textContent = "IP: " + response.responseText.trim();
} else {
this.ipDisplay.textContent = "IP: 获取失败";
}
},
onerror: () => {
this.ipDisplay.textContent = "IP: 获取失败";
},
});
},
setupEventListeners() {
// 拖拽相关事件
this.toggleButton.addEventListener("mousedown", this.onMouseDown.bind(this));
document.addEventListener("mousemove", this.onMouseMove.bind(this));
document.addEventListener("mouseup", this.onMouseUp.bind(this));
// 状态管理对象
this.state = {
isButtonHovered: false,
isDropdownHovered: false,
isDropdownVisible: false,
isDragging: false,
isProcessingClick: false, // 新增:处理点击状态
isClosing: false, // 新增:窗口正在关闭的状态
// 检查当前是否应该保持弹窗显示
shouldKeepOpen() {
// 修改逻辑,即使在拖动过程中,也要考虑鼠标悬停状态
return this.isButtonHovered || this.isDropdownHovered;
}
};
// 定时器
this.hoverTimeout = null;
this.closeTimeout = null;
// 鼠标进入按钮
this.toggleButton.addEventListener("mouseenter", (e) => {
if (this.state.isProcessingClick) return; // 如果正在处理点击,忽略hover
this.state.isButtonHovered = true;
clearTimeout(this.closeTimeout);
clearTimeout(this.hoverTimeout); // 确保清除之前的hover定时器
// 如果下拉窗口未显示或正在关闭中,则显示窗口
if (!this.state.isDropdownVisible || this.state.isClosing) {
// 如果窗口正在关闭,立即显示
if (this.state.isClosing) {
this.state.isClosing = false;
this.showDropdown();
} else {
this.hoverTimeout = setTimeout(() => {
this.showDropdown();
}, 300);
}
}
});
// 鼠标离开按钮
this.toggleButton.addEventListener("mouseleave", (e) => {
if (this.state.isProcessingClick) return; // 如果正在处理点击,忽略hover
this.state.isButtonHovered = false;
clearTimeout(this.hoverTimeout);
// 检查是否应该关闭弹窗
if (!this.state.shouldKeepOpen()) {
this.scheduleHideDropdown();
}
});
// 按钮点击事件
this.toggleButton.addEventListener("click", (e) => {
if (this.state.isDragging) return; // 如果正在拖拽,忽略点击
this.state.isProcessingClick = true;
clearTimeout(this.hoverTimeout);
clearTimeout(this.closeTimeout);
if (this.state.isDropdownVisible) {
this.hideDropdown();
} else {
this.showDropdown();
}
setTimeout(() => {
this.state.isProcessingClick = false;
}, 100);
});
// 鼠标进入弹窗
this.dropdownContainer.addEventListener("mouseenter", () => {
if (this.state.isProcessingClick) return;
this.state.isDropdownHovered = true;
clearTimeout(this.closeTimeout);
// 如果弹窗在淡出过程中,恢复显示
if (this.state.isDropdownVisible && this.dropdownContainer.style.opacity !== "1") {
this.dropdownContainer.style.opacity = "1";
this.dropdownContainer.style.transform = "scale(1)";
}
});
// 鼠标离开弹窗
this.dropdownContainer.addEventListener("mouseleave", () => {
if (this.state.isProcessingClick) return;
this.state.isDropdownHovered = false;
// 检查是否应该关闭弹窗
if (!this.state.shouldKeepOpen()) {
this.scheduleHideDropdown();
}
});
// 点击其他区域关闭下拉菜单
document.addEventListener("click", (e) => {
if (
this.dropdownContainer.style.display === "flex" &&
!this.dropdownContainer.contains(e.target) &&
e.target !== this.toggleButton
) {
this.state.isDropdownHovered = false;
this.state.isButtonHovered = false;
this.hideDropdown();
}
});
},
onMouseDown(e) {
if (e.button !== 0) return; // 只处理左键点击
this.isDragging = true;
this.state.isDragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
const rect = this.toggleButton.getBoundingClientRect();
this.offsetX = this.startX - rect.left;
this.offsetY = this.startY - rect.top;
this.toggleButton.style.cursor = "grabbing";
// 不再清除悬停定时器,允许在拖动过程中显示窗口
// clearTimeout(this.hoverTimeout);
clearTimeout(this.closeTimeout);
// 如果鼠标在按钮上,立即显示下拉窗口
if (this.state.isButtonHovered && !this.state.isDropdownVisible) {
this.showDropdown();
}
// 阻止默认行为和事件冒泡
e.preventDefault();
e.stopPropagation();
},
onMouseMove(e) {
// 检查鼠标是否在按钮上,用于处理拖动过程中的悬停
const buttonRect = this.toggleButton.getBoundingClientRect();
const isOverButton =
e.clientX >= buttonRect.left &&
e.clientX <= buttonRect.right &&
e.clientY >= buttonRect.top &&
e.clientY <= buttonRect.bottom;
// 更新悬停状态
if (isOverButton && !this.state.isButtonHovered) {
this.state.isButtonHovered = true;
// 如果下拉窗口未显示或正在关闭中,则显示窗口
if (!this.state.isDropdownVisible || this.state.isClosing) {
clearTimeout(this.hoverTimeout);
// 如果窗口正在关闭或正在拖动,立即显示
if (this.state.isClosing || this.isDragging) {
this.state.isClosing = false;
this.showDropdown();
} else {
this.hoverTimeout = setTimeout(() => {
this.showDropdown();
}, 300);
}
}
} else if (!isOverButton && this.state.isButtonHovered) {
this.state.isButtonHovered = false;
clearTimeout(this.hoverTimeout);
if (!this.state.shouldKeepOpen()) {
this.scheduleHideDropdown();
}
}
if (!this.isDragging) return;
const x = e.clientX - this.offsetX;
const y = e.clientY - this.offsetY;
// 计算底部位置
const bottom = window.innerHeight - y - this.toggleButton.offsetHeight;
// 确保按钮在窗口范围内
const maxX = window.innerWidth - this.toggleButton.offsetWidth;
const maxBottom = window.innerHeight - this.toggleButton.offsetHeight;
this.buttonLeft = Math.max(0, Math.min(x, maxX));
this.buttonBottom = Math.max(0, Math.min(bottom, maxBottom));
// 更新按钮位置
this.toggleButton.style.left = `${this.buttonLeft}px`;
this.toggleButton.style.bottom = `${this.buttonBottom}px`;
this.toggleButton.style.top = "auto";
// 如果下拉窗口可见,更新其位置
if (this.state.isDropdownVisible) {
this.updateDropdownPosition();
}
e.preventDefault();
},
// 添加新方法用于更新下拉窗口位置
updateDropdownPosition() {
const buttonRect = this.toggleButton.getBoundingClientRect();
const dropdownWidth = 600; // 下拉窗口宽度
// 计算下拉窗口位置
let left = buttonRect.right + 10;
let top = buttonRect.top;
// 检查是否超出右边界
if (left + dropdownWidth > window.innerWidth) {
// 如果右边放不下,则放到左边
left = buttonRect.left - dropdownWidth - 10;
}
// 如果左边也放不下,则居中显示
if (left < 0) {
left = Math.max(0, (window.innerWidth - dropdownWidth) / 2);
}
// 检查是否超出底部边界
const dropdownHeight = Math.min(this.dropdownContainer.scrollHeight, window.innerHeight * 0.8);
if (top + dropdownHeight > window.innerHeight) {
// 如果超出底部,则向上显示,确保完全可见
top = Math.max(0, window.innerHeight - dropdownHeight - 10);
}
// 应用新位置
this.dropdownContainer.style.left = `${left}px`;
this.dropdownContainer.style.top = `${top}px`;
},
onMouseUp(e) {
if (!this.isDragging) return;
this.isDragging = false;
this.state.isDragging = false;
this.toggleButton.style.cursor = "move";
// 保存位置
GM_setValue("buttonLeft", this.buttonLeft);
GM_setValue("buttonBottom", this.buttonBottom);
// 检查鼠标是否在按钮上
const buttonRect = this.toggleButton.getBoundingClientRect();
const isOverButton =
e.clientX >= buttonRect.left &&
e.clientX <= buttonRect.right &&
e.clientY >= buttonRect.top &&
e.clientY <= buttonRect.bottom;
// 更新悬停状态
this.state.isButtonHovered = isOverButton;
// 添加短暂延迟,避免与点击事件冲突
setTimeout(() => {
// 如果鼠标在按钮上且下拉窗口未显示,则显示下拉窗口
if (isOverButton && !this.state.isDropdownVisible) {
this.showDropdown();
}
// 否则检查是否应该关闭弹窗
else if (!this.state.shouldKeepOpen()) {
this.scheduleHideDropdown();
}
}, 100);
e.preventDefault();
},
scheduleHideDropdown() {
if (this.state.isProcessingClick) return;
clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
if (!this.state.shouldKeepOpen()) {
this.hideDropdown();
}
}, 300);
},
showDropdown() {
// 立即更新状态
this.state.isDropdownVisible = true;
this.state.isClosing = false;
// 计算下拉菜单位置
const buttonRect = this.toggleButton.getBoundingClientRect();
const dropdownWidth = 600;
// 先显示容器但设为透明,以便获取实际高度
this.dropdownContainer.style.opacity = "0";
this.dropdownContainer.style.display = "flex";
// 更新下拉窗口位置
this.updateDropdownPosition();
// 淡入效果
setTimeout(() => {
this.dropdownContainer.style.opacity = "1";
this.dropdownContainer.style.transform = "scale(1)";
this.toggleButton.style.transform = "scale(1.1)";
}, 10);
},
hideDropdown() {
// 设置正在关闭状态
this.state.isClosing = true;
// 添加动画
this.dropdownContainer.style.opacity = "0";
this.dropdownContainer.style.transform = "scale(0.95)";
this.toggleButton.style.transform = "scale(1)";
// 等待动画完成后隐藏
this.closeTimeout = setTimeout(() => {
if (!this.state.shouldKeepOpen()) {
this.dropdownContainer.style.display = "none";
this.state.isDropdownVisible = false;
this.state.isClosing = false; // 重置关闭状态
} else {
// 如果此时应该保持打开,则恢复显示
this.state.isClosing = false; // 重置关闭状态
this.dropdownContainer.style.opacity = "1";
this.dropdownContainer.style.transform = "scale(1)";
}
}, 300);
},
observeThemeChanges() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === "data-mode"
) {
this.isDarkMode =
document.documentElement.getAttribute("data-mode") === "dark";
this.updateStyles();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-mode"],
});
},
};
// 初始化应用
App.init();
})();