// ==UserScript==
// @name [Claude] Session Key (Token) \切换&管理/ 20241028.2
// @version 20241028.2
// @description 带批量测活、更用户友好的 [Claude] Session Key (Token) 管理
// @author xiaohan17, ethan-j, 0_V, ... and Luminus (+ AI assistant)
//
// @match https://claude.ai/*
// @match https://claude.asia/*
// @match https://demo.fuclaude.com/*
// @include https://*fuclaude*/*
//
// @icon https://claude.ai/favicon.ico
//
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
//
// @connect ipapi.co
// @connect api.claude.ai
// @namespace https://greasyforks.org/users/1387562
// ==/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-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: 10000;
}
.claude-modal-content {
background-color: var(--bg-color);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 500px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
.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, .claude-modal select {
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);
transition: border-color 0.3s ease;
}
.claude-modal input:focus, .claude-modal textarea:focus, .claude-modal select:focus {
outline: none;
border-color: #6e6e6e;
}
.claude-modal button {
padding: 10px 16px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s, transform 0.1s;
font-size: 14px;
font-weight: 500;
}
.claude-modal button:active {
transform: scale(0.98);
}
.claude-button-save {
background-color: #b3462f;
color: #ffffff;
}
.claude-button-save:hover {
background-color: #a03d2a;
}
.claude-button-cancel {
background-color: var(--button-bg);
color: var(--text-color);
}
.claude-button-cancel:hover {
background-color: var(--button-hover-bg);
}
.claude-button-container {
display: grid;
grid-template-columns: repeat(4, 1fr); // 为4列均分
gap: 12px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
.claude-button-container button {
min-width: 100px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
transition: all 0.2s ease;
}
.claude-button-container .claude-button-save {
background-color: #b3462f;
color: white;
}
.claude-button-container .claude-button-save:hover {
background-color: #a03d2a;
}
.claude-button-container .claude-button-cancel {
background-color: var(--button-bg);
color: var(--text-color);
}
.claude-button-container .claude-button-cancel:hover {
background-color: var(--button-hover-bg);
}
.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-list {
max-height: 400px;
overflow-y: auto;
margin-bottom: 20px;
padding-right: 12px;
}
.claude-token-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
margin-bottom: 12px;
border-radius: 12px;
background-color: var(--bg-color);
border: 1px solid var(--border-color);
transition: all 0.2s ease;
}
.claude-token-item:hover {
/* transform: translateY(-2px); // 移除上移动画 */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.claude-token-actions {
display: flex;
align-items: center;
gap: 16px;
}
.claude-token-icon {
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s ease, transform 0.2s ease;
font-size: 16px;
}
.claude-token-icon:hover {
opacity: 1;
transform: scale(1.1);
}
.test-status {
padding: 4px 12px;
border-radius: 6px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
.test-status.success {
background-color: rgba(72, 187, 120, 0.1);
color: #48bb78;
}
.test-status.error {
background-color: rgba(245, 101, 101, 0.1);
color: #f56565;
}
.loading-spinner {
width: 14px;
height: 14px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.claude-add-token-button, .claude-bulk-import-button {
margin-top: 15px;
width: 100%;
}
#claude-toggle-button {
position: fixed;
top: 10px;
right: 200px;
z-index: 9998;
width: 120px;
height: 36px;
background-color: transparent;
border: none;
border-radius: 0.375rem;
font-size: 15px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease, color 0.3s ease;
color: var(--text-color);
}
#claude-toggle-button:hover {
background-color: var(--button-hover-bg);
}
#claude-container {
position: fixed;
top: 50px;
right: 77px;
z-index: 9999;
background-color: var(--bg-color);
padding: 12px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: none;
font-size: 14px;
width: auto;
}
#claude-token-select {
margin-right: 8px;
font-size: 14px;
width: 150px;
background-color: var(--bg-color);
color: var(--text-color);
height: 36px;
padding: 0 8px;
line-height: 36px;
border: 1px solid var(--border-color);
border-radius: 4px;
appearance: none;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 12px;
}
#claude-switch-button, #claude-manage-button {
font-size: 14px;
height: 36px;
padding: 0 12px;
line-height: 34px;
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s;
}
#claude-switch-button:active, #claude-manage-button:active {
transform: scale(0.98);
}
.claude-preview-container {
margin-top: 15px;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 15px;
background-color: var(--bg-color);
}
.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);
}
.claude-preview-item:nth-child(even) {
background-color: var(--button-hover-bg);
}
.claude-preview-name {
font-weight: bold;
color: #b3462f;
}
.claude-preview-key {
font-family: monospace;
word-break: break-all;
}
.claude-naming-rule {
display: flex;
flex-direction: column;
margin-bottom: 15px;
}
.claude-naming-rule label {
margin-bottom: 5px;
color: var(--text-color);
}
.claude-naming-rule select, .claude-naming-rule input {
width: 100%;
margin-bottom: 10px;
}
.claude-token-item.test-success {
background-color: rgba(125, 74, 59, 0.05);
border-color: rgba(125, 74, 59, 0.3);
box-shadow: 0 2px 4px rgba(125, 74, 59, 0.1);
}
.claude-token-item.test-error {
background-color: rgba(96, 96, 96, 0.05);
border-color: rgba(96, 96, 96, 0.2);
box-shadow: 0 2px 4px rgba(96, 96, 96, 0.1);
}
.claude-token-item .test-status {
margin-left: 8px;
display: inline-flex;
align-items: center;
}
.claude-token-item .loading-spinner {
width: 16px;
height: 16px;
border: 2px solid var(--border-color);
border-top: 2px solid #b3462f;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 4px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.test-result-icon {
font-size: 16px;
margin-right: 4px;
}
.test-result-text {
font-size: 14px;
color: var(--text-color);
}
/* 滚动条样式 */
.claude-token-list::-webkit-scrollbar,
.claude-preview-container::-webkit-scrollbar,
.claude-modal-content::-webkit-scrollbar {
width: 6px;
}
.claude-token-list::-webkit-scrollbar-track,
.claude-preview-container::-webkit-scrollbar-track,
.claude-modal-content::-webkit-scrollbar-track {
background: transparent;
}
.claude-token-list::-webkit-scrollbar-thumb,
.claude-preview-container::-webkit-scrollbar-thumb,
.claude-modal-content::-webkit-scrollbar-thumb {
background-color: ${
isDarkMode
? "rgba(255, 255, 255, 0.2)"
: "rgba(0, 0, 0, 0.2)"
};
border-radius: 3px;
}
.claude-token-list::-webkit-scrollbar-thumb:hover,
.claude-preview-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)"
};
}
.token-action-btn {
padding: 6px 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: transparent;
color: var(--text-color);
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.token-action-btn:hover {
background-color: var(--button-hover-bg);
transform: translateY(-1px);
}
.token-action-btn.edit-token-button {
color: #4a90e2;
border-color: #4a90e2;
}
.token-action-btn.delete-token-button {
color: #e24a4a;
border-color: #e24a4a;
}
.token-action-btn.edit-token-button:hover {
background-color: rgba(74, 144, 226, 0.1);
}
.token-action-btn.delete-token-button:hover {
background-color: rgba(226, 74, 74, 0.1);
}
.test-status {
padding: 4px 12px;
border-radius: 6px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
.test-status.success {
background-color: rgba(72, 187, 120, 0.1);
color: #48bb78;
}
.test-status.error {
background-color: rgba(245, 101, 101, 0.1);
color: #f56565;
}
.token-action-icon {
cursor: pointer;
padding: 6px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.token-action-icon:hover {
background-color: var(--button-hover-bg);
transform: translateY(-1px);
}
.token-action-icon.edit-token-button {
color: var(--text-color);
}
.token-action-icon.delete-token-button {
color: #e24a4a;
}
.token-action-icon svg {
width: 16px;
height: 16px;
}
// 编辑模态框样式
.claude-edit-token-form {
display: flex;
flex-direction: column;
gap: 16px;
padding: 10px;
}
.claude-edit-token-form input {
background-color: ${
isDarkMode
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.02)"
};
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px 16px;
font-size: 14px;
transition: all 0.2s ease;
}
.claude-edit-token-form input:focus {
border-color: #b3462f;
box-shadow: 0 0 0 2px rgba(179, 70, 47, 0.1);
outline: none;
}
.claude-edit-token-form label {
display: block;
margin-bottom: 6px;
color: var(--text-color);
font-size: 14px;
font-weight: 500;
}
// 删除确认模态框样式
.claude-delete-confirm {
text-align: center;
padding: 20px 10px;
}
.claude-delete-confirm-icon {
font-size: 48px;
color: #e24a4a;
margin-bottom: 16px;
}
.claude-delete-confirm-title {
font-size: 18px;
font-weight: 600;
color: var(--text-color);
margin-bottom: 12px;
}
.claude-delete-confirm-message {
font-size: 14px;
color: ${
isDarkMode
? "rgba(255, 255, 255, 0.7)"
: "rgba(0, 0, 0, 0.6)"
};
margin-bottom: 24px;
line-height: 1.5;
}
.claude-delete-confirm-buttons {
display: flex;
justify-content: center;
gap: 12px;
}
.claude-delete-confirm-buttons button {
min-width: 100px;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
transition: all 0.2s ease;
}
.claude-delete-button {
background-color: #e24a4a;
color: white;
border: none;
}
.claude-delete-button:hover {
background-color: #d43c3c;
}
.claude-token-item {
cursor: pointer;
transition: all 0.2s ease;
}
.claude-token-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.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;
right: -8px;
background-color: #b3462f;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.claude-help-text {
text-align: center;
color: var(--text-color);
opacity: 0.8;
margin-bottom: 15px;
font-size: 14px;
padding: 8px;
background-color: var(--button-bg);
border-radius: 6px;
}
#claude-toggle-button {
min-width: 160px;
background-color: var(--button-bg);
border: 1px solid var(--border-color);
}
`;
const UI = {
createElem(tag, styles) {
const elem = document.createElement(tag);
Object.assign(elem.style, styles);
return elem;
},
createButton(text, styles, className) {
const button = this.createElem("button", styles);
button.textContent = text;
button.className = className;
return button;
},
createModal(title, content, includeCloseButton = false) {
const modal = document.createElement("div");
modal.className = "claude-modal";
modal.setAttribute("aria-modal", "true");
modal.setAttribute("role", "dialog");
const modalContent = document.createElement("div");
modalContent.className = "claude-modal-content";
const titleElem = document.createElement("h2");
titleElem.textContent = title;
modalContent.appendChild(titleElem);
if (includeCloseButton) {
const closeButton = document.createElement("button");
closeButton.textContent = "X";
closeButton.className = "claude-close-button";
modalContent.appendChild(closeButton);
}
modalContent.appendChild(content);
const buttonContainer = document.createElement("div");
buttonContainer.className = "claude-button-container";
// 添加样式
buttonContainer.style.borderTop = "1px solid var(--border-color)";
buttonContainer.style.paddingTop = "15px";
buttonContainer.style.marginTop = "15px";
modalContent.appendChild(buttonContainer);
modal.appendChild(modalContent);
document.body.appendChild(modal);
return {
modal,
buttonContainer,
close: () => document.body.removeChild(modal),
};
},
createTestButton() {
const button = this.createButton("测试", {}, "claude-button-cancel");
button.style.marginLeft = "8px";
return button;
},
createTestResultSpan() {
const span = document.createElement("span");
span.style.marginLeft = "8px";
span.style.fontSize = "14px";
return span;
},
};
const App = {
init() {
this.isDarkMode =
document.documentElement.getAttribute("data-mode") === "dark";
this.injectStyles();
this.tokens = this.loadTokens();
this.createUI();
this.setupEventListeners();
this.fetchIPCountryCode();
this.observeThemeChanges();
this.selectCurrentToken();
},
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);
return savedTokens && savedTokens.length > 0
? savedTokens
: [config.defaultToken];
} catch (error) {
console.error("Error loading tokens:", error);
return [config.defaultToken];
}
},
saveTokens() {
try {
GM_setValue(config.storageKey, this.tokens);
} catch (error) {
console.error("Error saving tokens:", error);
alert("Failed to save tokens. Please try again.");
}
},
createUI() {
this.toggleButton = UI.createButton("", {}, "claude-button-cancel");
this.toggleButton.id = "claude-toggle-button";
this.toggleButton.addEventListener("click", () =>
this.showManageTokensModal()
);
// 初始化时就显示IP
this.fetchIPCountryCode();
document.body.appendChild(this.toggleButton);
},
setupEventListeners() {
// 使用事件委托,但要处理SVG元素的情况
document.addEventListener("click", (event) => {
const actionButton = event.target.closest(
".delete-token-button, .edit-token-button"
);
if (!actionButton) return;
event.stopPropagation(); // 阻止事件冒泡,防止触发卡片的点击事件
if (actionButton.classList.contains("delete-token-button")) {
this.confirmDeleteToken(actionButton.dataset.index);
} else if (actionButton.classList.contains("edit-token-button")) {
this.showEditTokenModal(actionButton.dataset.index);
}
});
// 其他事件监听保持不变
document.addEventListener("click", (event) => {
if (event.target.classList.contains("claude-add-token-button")) {
this.showAddTokenModal();
} else if (
event.target.classList.contains("claude-bulk-import-button")
) {
this.showBulkImportModal();
}
});
},
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"],
});
},
updateTokenSelect() {
this.updateManageTokensModal();
},
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;
}
}
},
showManageTokensModal() {
const content = document.createElement("div");
content.className = "claude-token-manager";
const tokenList = this.createTokenList();
content.appendChild(tokenList);
const { modal, buttonContainer, close } = UI.createModal(
"📚 Session 管理",
content,
true
);
// 添加说明文本
const helpText = document.createElement("div");
helpText.className = "claude-help-text";
helpText.textContent = "点击任意 Session 卡片即可切换使用";
content.insertBefore(helpText, tokenList);
// 添加关闭按钮事件
const closeButton = modal.querySelector(".claude-close-button");
if (closeButton) {
closeButton.addEventListener("click", close);
}
modal.addEventListener("click", (event) => {
if (event.target === modal) {
close();
}
});
// 设置 buttonContainer 为垂直布局
buttonContainer.style.display = "flex";
buttonContainer.style.flexDirection = "column";
buttonContainer.style.width = "100%";
// 创建两个按钮容器来分行显示
const topButtonRow = document.createElement("div");
topButtonRow.style.cssText = "margin-bottom: 12px; width: 100%;";
const bottomButtonRow = document.createElement("div");
bottomButtonRow.style.cssText =
"display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; width: 100%;";
// 一键测活按钮
const testAllButton = UI.createButton(
"🔍 一键测活",
{},
"claude-button-save"
);
testAllButton.style.width = "100%";
testAllButton.addEventListener("click", () => this.testAllTokens());
topButtonRow.appendChild(testAllButton);
// 其他按钮
const removeInvalidButton = UI.createButton(
"🗑️ 清理无效",
{},
"claude-button-cancel"
);
removeInvalidButton.style.marginLeft = "0"; // 设置左边距为0
removeInvalidButton.style.marginRight = "0"; // 设置右边距为0
const addTokenButton = UI.createButton(
"📮 单个添加",
{},
"claude-button-cancel"
);
addTokenButton.style.marginLeft = "0"; // 设置左边距为0
addTokenButton.style.marginRight = "0"; // 设置右边距为0
const bulkImportButton = UI.createButton(
"📥 批量导入",
{},
"claude-button-cancel"
);
bulkImportButton.style.marginLeft = "0"; // 设置左边距为0
bulkImportButton.style.marginRight = "0"; // 设置右边距为0
const exportButton = UI.createButton(
"📤 批量导出",
{},
"claude-button-cancel"
);
exportButton.style.marginLeft = "0"; // 设置左边距为0
exportButton.style.marginRight = "0"; // 设置右边距为0
removeInvalidButton.addEventListener("click", () =>
this.removeInvalidTokens()
);
addTokenButton.addEventListener("click", () => this.showAddTokenModal());
bulkImportButton.addEventListener("click", () =>
this.showBulkImportModal()
);
exportButton.addEventListener("click", () => this.exportTokens());
// 将按钮添加到底部按钮行
bottomButtonRow.appendChild(removeInvalidButton);
bottomButtonRow.appendChild(addTokenButton);
bottomButtonRow.appendChild(bulkImportButton);
bottomButtonRow.appendChild(exportButton);
buttonContainer.appendChild(topButtonRow);
buttonContainer.appendChild(bottomButtonRow);
},
createTokenList() {
const tokenList = document.createElement("div");
tokenList.className = "claude-token-list";
this.tokens.forEach((token, index) => {
const tokenItem = this.createTokenItem(token, index);
tokenList.appendChild(tokenItem);
});
return tokenList;
},
createTokenItem(token, index) {
const tokenItem = document.createElement("div");
tokenItem.className = "claude-token-item";
tokenItem.dataset.index = index;
// 检查是否是当前使用的token
const currentTokenName = GM_getValue(config.currentTokenKey);
if (token.name === currentTokenName) {
tokenItem.classList.add("current-token");
// 添加"当前"标签
const currentBadge = document.createElement("div");
currentBadge.className = "current-token-badge";
currentBadge.textContent = "当前";
tokenItem.appendChild(currentBadge);
}
// 左侧信息容器
const infoContainer = document.createElement("div");
infoContainer.className = "token-info";
infoContainer.style.cssText =
"display: flex; flex-direction: column; gap: 8px; flex: 1;";
// 第一行:序号、名称和操作按钮
const topRow = document.createElement("div");
topRow.style.cssText =
"display: flex; align-items: center; justify-content: space-between;";
// 左侧序号和名称
const nameContainer = document.createElement("div");
nameContainer.style.cssText =
"display: flex; align-items: center; gap: 12px;";
const numberBadge = document.createElement("span");
numberBadge.className = "token-number";
numberBadge.textContent = `#${(index + 1).toString().padStart(2, "0")}`;
numberBadge.style.cssText =
"padding: 2px 8px; border-radius: 12px; font-size: 12px; background-color: var(--button-bg);";
const nameSpan = document.createElement("span");
nameSpan.textContent = token.name;
nameSpan.style.cssText = "font-weight: 500; font-size: 14px;";
nameContainer.appendChild(numberBadge);
nameContainer.appendChild(nameSpan);
// 右侧编辑和删除按钮
const actionButtons = document.createElement("div");
actionButtons.style.cssText = "display: flex; gap: 8px;";
const editButton = document.createElement("button");
editButton.className = "token-action-btn edit-token-button";
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", (event) => {
event.stopPropagation(); // 阻止事件冒泡,防止触发卡片的点击事件
this.showEditTokenModal(index);
});
const deleteButton = document.createElement("button");
deleteButton.className = "token-action-btn delete-token-button";
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", (event) => {
event.stopPropagation(); // 阻止事件冒泡,防止触发卡片的点击事件
this.confirmDeleteToken(index);
});
actionButtons.appendChild(editButton);
actionButtons.appendChild(deleteButton);
topRow.appendChild(nameContainer);
topRow.appendChild(actionButtons);
infoContainer.appendChild(topRow);
// 第二行:测试状态和时间
const bottomRow = document.createElement("div");
bottomRow.style.cssText =
"display: flex; align-items: center; gap: 12px;";
const testStatus = document.createElement("span");
testStatus.className = "test-status untested";
testStatus.style.cursor = "pointer";
const timeStamp = document.createElement("span");
timeStamp.className = "time-stamp";
timeStamp.style.cssText =
"font-size: 12px; color: var(--text-color); opacity: 0.7;";
// 检查缓存的测试结果
const cachedResult = this.getTestResult(token.key);
if (cachedResult) {
this.updateTestStatus(testStatus, timeStamp, cachedResult);
} else {
// 修改默认状态为 untested
testStatus.className = "test-status untested";
testStatus.innerHTML =
'<span style="font-size: 10px;">●</span><span>未测试</span>';
timeStamp.textContent = ""; // 未测试时不显示时间
}
// 添加点击测试功能
testStatus.addEventListener("click", async (event) => {
event.stopPropagation(); // 阻止事件冒泡
if (!testStatus.classList.contains("loading")) {
this.showLoadingSpinner(testStatus);
const result = await this.testToken(token.key);
this.saveTestResult(token.key, result);
this.updateTestStatus(testStatus, timeStamp, result);
}
});
bottomRow.appendChild(testStatus);
bottomRow.appendChild(timeStamp);
infoContainer.appendChild(bottomRow);
tokenItem.appendChild(infoContainer);
// 修改点击切换功能
tokenItem.addEventListener("click", async () => {
// 先检查是否有缓存的测试结果
const cachedResult = this.getTestResult(token.key);
// 如果有缓存的测试结果且为无效,提示用户并询问是否继续
if (cachedResult && cachedResult.status === "error") {
const confirmResult = await this.showConfirmDialog(
"警告",
`该 Token "${token.name}" 已被标记为无效,是否仍要切换到该 Token?`
);
if (!confirmResult) {
return;
}
}
// 如果没有缓存结果或结果已过期,进��实时测试
if (!cachedResult) {
const testStatus = tokenItem.querySelector(".test-status");
this.showLoadingSpinner(testStatus);
const result = await this.testToken(token.key);
this.saveTestResult(token.key, result);
// 如果测试结果为无效,提示用户并询问是否继续
if (result.status === "error") {
this.updateTestStatus(
testStatus,
tokenItem.querySelector(".time-stamp"),
result
);
const confirmResult = await this.showConfirmDialog(
"警告",
`测试显示该 Token "${token.name}" 无效,是否仍要切换到该 Token?`
);
if (!confirmResult) {
return;
}
}
}
// Token 有效或用户确认要使用无效token,执行切换操作
this.applyToken(token.key);
GM_setValue(config.currentTokenKey, token.name);
// 更新高亮显示
document.querySelectorAll(".claude-token-item").forEach((item) => {
item.classList.remove("current-token");
// 移除所有现有的 current-token-badge
const badge = item.querySelector(".current-token-badge");
if (badge) {
badge.remove();
}
});
tokenItem.classList.add("current-token");
// 添加"当前"标签
if (!tokenItem.querySelector(".current-token-badge")) {
const currentBadge = document.createElement("div");
currentBadge.className = "current-token-badge";
currentBadge.textContent = "当前";
tokenItem.appendChild(currentBadge);
}
});
return tokenItem;
},
showAddTokenModal() {
const content = document.createElement("div");
content.className = "claude-add-token-form";
const nameInput = document.createElement("input");
nameInput.placeholder = "Token 名称";
nameInput.setAttribute("aria-label", "Token 名称:");
content.appendChild(nameInput);
const keyInput = document.createElement("input");
keyInput.placeholder = "Token 密钥";
keyInput.setAttribute("aria-label", "Token 密钥:");
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-save");
addButton.addEventListener("click", () => {
if (this.validateInput(nameInput.value, keyInput.value)) {
// 添加新token时不进行测试,也不添加测试结果缓存
this.tokens.push({
name: nameInput.value,
key: keyInput.value,
});
// 确保不会有该token的缓存测试结果
const testResults = this.loadTestResults();
if (testResults[keyInput.value]) {
delete testResults[keyInput.value];
GM_setValue(config.testResultsKey, testResults);
}
this.saveTokens();
this.updateTokenSelect();
this.updateManageTokensModal();
close();
}
});
buttonContainer.appendChild(addButton);
const cancelButton = UI.createButton("取消", {}, "claude-button-cancel");
cancelButton.addEventListener("click", close);
buttonContainer.appendChild(cancelButton);
},
showEditTokenModal(index) {
const token = this.tokens[index];
const content = document.createElement("div");
content.className = "claude-edit-token-form";
// 名称输入区域
const nameGroup = document.createElement("div");
const nameLabel = document.createElement("label");
nameLabel.textContent = "Token 名称";
const nameInput = document.createElement("input");
nameInput.value = token.name;
nameInput.placeholder = "请输入 Token 名称";
nameGroup.appendChild(nameLabel);
nameGroup.appendChild(nameInput);
// 密钥输入区域
const keyGroup = document.createElement("div");
const keyLabel = document.createElement("label");
keyLabel.textContent = "Token 密钥";
const keyInput = document.createElement("input");
keyInput.value = token.key;
keyInput.placeholder = "请输入 Token 密钥";
keyGroup.appendChild(keyLabel);
keyGroup.appendChild(keyInput);
content.appendChild(nameGroup);
content.appendChild(keyGroup);
const { modal, buttonContainer, close } = UI.createModal(
"✏️ 编辑 Token",
content
);
modal
.querySelector(".claude-modal-content")
.classList.add("narrow-modal");
const saveButton = UI.createButton("保存", {}, "claude-button-save");
saveButton.addEventListener("click", () => {
if (this.validateInput(nameInput.value, keyInput.value)) {
this.tokens[index] = { name: nameInput.value, key: keyInput.value };
this.saveTokens();
this.updateTokenSelect();
this.updateManageTokensModal();
close();
}
});
const cancelButton = UI.createButton("取消", {}, "claude-button-cancel");
cancelButton.addEventListener("click", close);
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(saveButton);
},
showBulkImportModal() {
const content = document.createElement("div");
content.className = "claude-bulk-import-form";
const textareaLabel = document.createElement("label");
textareaLabel.innerHTML =
"<strong>1️⃣ Tokens 粘贴区:</strong><br>" +
"在这里粘贴您需要导入的 Tokens,每行一个!";
textareaLabel.setAttribute("for", "claude-bulk-import-textarea");
content.appendChild(textareaLabel);
const textarea = document.createElement("textarea");
textarea.id = "claude-bulk-import-textarea";
textarea.placeholder = "";
textarea.rows = 10;
content.appendChild(textarea);
const namingRuleContainer = document.createElement("div");
namingRuleContainer.className = "claude-naming-rule";
const namingRuleContainerLabel = document.createElement("label");
namingRuleContainerLabel.innerHTML =
"<strong>2️⃣ Tokens 命名规则:</strong>";
namingRuleContainerLabel.setAttribute("for", "claude-naming-rule");
namingRuleContainer.appendChild(namingRuleContainerLabel);
const nameLabel = document.createElement("label");
nameLabel.textContent = "名称前缀:";
nameLabel.setAttribute("for", "claude-name-prefix");
namingRuleContainer.appendChild(nameLabel);
const nameInput = document.createElement("input");
nameInput.id = "claude-name-prefix";
nameInput.value = "token";
namingRuleContainer.appendChild(nameInput);
const numberLabel = document.createElement("label");
numberLabel.textContent = "名称起始编号:";
numberLabel.setAttribute("for", "claude-start-number");
namingRuleContainer.appendChild(numberLabel);
const numberInput = document.createElement("input");
numberInput.id = "claude-start-number";
numberInput.type = "number";
numberInput.value = "1";
namingRuleContainer.appendChild(numberInput);
const orderLabel = document.createElement("label");
orderLabel.textContent = "名称排序方式:";
orderLabel.setAttribute("for", "claude-order-select");
namingRuleContainer.appendChild(orderLabel);
const orderSelect = document.createElement("select");
orderSelect.id = "claude-order-select";
orderSelect.innerHTML = `
<option value="asc">顺序</option>
<option value="desc">倒序</option>
`;
namingRuleContainer.appendChild(orderSelect);
content.appendChild(namingRuleContainer);
const previewContainer = document.createElement("div");
previewContainer.className = "claude-preview-container";
const previewContainerLabel = document.createElement("label");
previewContainerLabel.innerHTML =
"<strong>3️⃣ Tokens 导入结果 预览:</strong>";
previewContainerLabel.setAttribute("for", "claude-preview-container");
content.appendChild(previewContainerLabel);
content.appendChild(previewContainer);
const { modal, buttonContainer, close } = UI.createModal(
"📥 批量入 Tokens",
content
);
const importButton = UI.createButton("导入", {}, "claude-button-save");
importButton.addEventListener("click", () => {
this.performBulkImport(
textarea.value,
nameInput.value,
numberInput.value,
orderSelect.value
);
close();
});
buttonContainer.appendChild(importButton);
const cancelButton = UI.createButton("取消", {}, "claude-button-cancel");
cancelButton.addEventListener("click", close);
buttonContainer.appendChild(cancelButton);
const updatePreview = () => {
this.previewBulkImport(
textarea.value,
nameInput.value,
numberInput.value,
orderSelect.value,
previewContainer
);
};
[textarea, nameInput, numberInput, orderSelect].forEach((elem) => {
elem.addEventListener("input", updatePreview);
});
updatePreview();
},
previewBulkImport(input, namePrefix, startNumber, order, previewContainer) {
const tokens = this.parseTokens(input);
const namedTokens = this.applyNamingRule(
tokens,
namePrefix,
parseInt(startNumber),
order
);
const claudePreviewTitle = document.createElement("div");
claudePreviewTitle.innerHTML =
'<div class="claude-preview-title">请核对下方导入结果:</div>';
previewContainer.appendChild(claudePreviewTitle);
namedTokens.forEach((token) => {
const previewItem = document.createElement("div");
previewItem.className = "claude-preview-item";
previewItem.innerHTML = `
<span class="claude-preview-name">${token.name}:</span>
<span class="claude-preview-key">${token.key}</span>
`;
previewContainer.appendChild(previewItem);
});
if (namedTokens.length === 0) {
const emptyMessage = document.createElement("div");
emptyMessage.className = "claude-preview-item";
emptyMessage.textContent = "等待... ";
previewContainer.appendChild(emptyMessage);
}
},
performBulkImport(input, namePrefix, startNumber, order) {
const tokens = this.parseTokens(input);
const namedTokens = this.applyNamingRule(
tokens,
namePrefix,
parseInt(startNumber),
order
);
this.tokens = [...this.tokens, ...namedTokens];
this.saveTokens();
this.updateTokenSelect();
this.updateManageTokensModal();
},
parseTokens(input) {
return input
.split("\n")
.map((line) => line.trim())
.filter((line) => this.validateTokenKey(line))
.map((key) => ({ key }));
},
applyNamingRule(tokens, namePrefix, startNumber, order) {
return tokens.map((token, index) => {
const number =
order === "asc"
? startNumber + index
: startNumber + tokens.length - 1 - index;
const name = `${namePrefix}${number.toString().padStart(2, "0")}`;
return { ...token, name };
});
},
updateManageTokensModal() {
const tokenList = document.querySelector(".claude-token-list");
if (tokenList) {
tokenList.innerHTML = "";
this.tokens.forEach((token, index) => {
const tokenItem = this.createTokenItem(token, index);
tokenList.appendChild(tokenItem);
});
}
},
confirmDeleteToken(index) {
const token = this.tokens[index];
const content = document.createElement("div");
content.className = "claude-delete-confirm";
content.innerHTML = `
<div class="claude-delete-confirm-icon">⚠️</div>
<div class="claude-delete-confirm-title">删除确认</div>
<div class="claude-delete-confirm-message">
您确定要删除 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-delete-button");
deleteButton.addEventListener("click", () => {
this.deleteToken(index);
close();
});
const cancelButton = UI.createButton("取消", {}, "claude-button-cancel");
cancelButton.addEventListener("click", close);
const buttonsContainer = document.createElement("div");
buttonsContainer.className = "claude-delete-confirm-buttons";
buttonsContainer.appendChild(cancelButton);
buttonsContainer.appendChild(deleteButton);
buttonContainer.appendChild(buttonsContainer);
},
deleteToken(index) {
this.tokens.splice(index, 1);
this.saveTokens();
this.updateTokenSelect();
this.updateManageTokensModal();
},
validateInput(name, key) {
if (!name || !key) {
alert("Token 名称和密钥都要填写!");
return false;
}
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);
},
fetchIPCountryCode: (() => {
let lastFetchTime = 0;
const FETCH_INTERVAL = 60000; // 1 minute
return function () {
const now = Date.now();
if (now - lastFetchTime < FETCH_INTERVAL) {
return;
}
lastFetchTime = now;
this.toggleButton.innerHTML =
"🌎 IP: <strong>Loading...</strong>";
GM_xmlhttpRequest({
method: "GET",
url: config.ipApiUrl,
onload: (response) => {
if (response.status === 200) {
this.toggleButton.innerHTML =
"🌎 IP: <strong>" +
response.responseText.trim() +
"</strong>";
} else {
this.toggleButton.innerHTML =
"🌎 IP: <strong>ERROR</strong>";
}
},
onerror: () => {
this.toggleButton.innerHTML =
"🌎 IP: <strong>ERROR</strong>";
},
});
};
})(),
selectCurrentToken() {
const currentTokenName = GM_getValue(config.currentTokenKey);
if (currentTokenName) {
const token = this.tokens.find(
(token) => token.name === currentTokenName
);
if (token) {
// 可以在这里添加其他需要的初始化逻辑
}
}
},
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: "超时" });
},
});
});
},
async testAllTokens() {
const tokenItems = document.querySelectorAll(
".claude-token-list .claude-token-item"
);
// 修改选择器以匹配新的布局结构
const testButton = document.querySelector(
".claude-modal .claude-button-container .claude-button-save"
);
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((tokenItem) => this.processTokenItem(tokenItem))
);
}
testButton.disabled = false;
testButton.textContent = "🔍 一键测活";
},
async processTokenItem(tokenItem) {
const index = tokenItem.dataset.index;
const token = this.tokens[index];
const testStatus = tokenItem.querySelector(".test-status");
const timeStamp = tokenItem.querySelector(".time-stamp");
// 显示加载动画
this.showLoadingSpinner(testStatus);
// 测试 token 并保存结果
const result = await this.testToken(token.key);
this.saveTestResult(token.key, result);
// 更新测试状态显示
this.updateTestStatus(testStatus, timeStamp, result);
},
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];
},
showLoadingSpinner(container) {
container.className = "test-status";
container.innerHTML = `
<div class="loading-spinner"></div>
<span>测试中</span>
`;
},
updateTestStatus(statusContainer, timeContainer, result) {
statusContainer.className = `test-status ${result.status}`;
const icon = result.status === "success" ? "●" : "●";
const text = result.status === "success" ? "有效" : "无效";
statusContainer.innerHTML = `
<span style="font-size: 10px;">${icon}</span>
<span>${text}</span>
`;
// 直接使用保存的格式化时间,如果没有则生成新的
timeContainer.textContent =
result.testTime ||
new Date().toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
},
// 添加删除无效token的方法
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.updateTokenSelect();
this.updateManageTokensModal();
},
// 添加确认对话框方法
showConfirmDialog(title, message) {
return new Promise((resolve) => {
const content = document.createElement("div");
content.className = "claude-delete-confirm";
content.innerHTML = `
<div class="claude-delete-confirm-icon">⚠️</div>
<div class="claude-delete-confirm-title">${title}</div>
<div class="claude-delete-confirm-message">${message}</div>
`;
const { modal, buttonContainer, close } = UI.createModal("", content);
modal
.querySelector(".claude-modal-content")
.classList.add("narrow-modal");
const confirmButton = UI.createButton(
"确认",
{},
"claude-delete-button"
);
confirmButton.addEventListener("click", () => {
close();
resolve(true);
});
const cancelButton = UI.createButton(
"取消",
{},
"claude-button-cancel"
);
cancelButton.addEventListener("click", () => {
close();
resolve(false);
});
const buttonsContainer = document.createElement("div");
buttonsContainer.className = "claude-delete-confirm-buttons";
buttonsContainer.appendChild(cancelButton);
buttonsContainer.appendChild(confirmButton);
buttonContainer.appendChild(buttonsContainer);
});
},
// 添加导出功能方法
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, // 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);
URL.revokeObjectURL(url);
},
};
// Initialize the application
App.init();
})();